package be.iminds.ilabt.jfed.testing.base;

import be.iminds.ilabt.jfed.call_log_output.LogOutput;
import be.iminds.ilabt.jfed.log.Logger;
import be.iminds.ilabt.jfed.lowlevel.GeniUser;
import be.iminds.ilabt.jfed.lowlevel.GeniUserProvider;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AutomaticAggregateManagerWrapper;
import be.iminds.ilabt.jfed.lowlevel.api_wrapper.impl.AutomaticUserAndSliceApiWrapper;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.lowlevel.connection.JFedConnectionProvider;
import be.iminds.ilabt.jfed.preferences.JFedPreferences;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.TargetAuthority;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.util.*;

/**
 * ApiTest:
 * <p>
 * for a test class you need:
 * - user info
 * - SA info if applicable
 * - tested API info
 * - connection (auto)
 * - logger (auto)
 * <p>
 * per test meta data
 * - name (default: auto from method name)
 * - dependencies and preferred grouping (name or methodName)
 * - hard dependency (skip if other failed or skipped)
 * - soft dependency (always run after other (no matter if it succeeds, skips or fails))
 * - group (a name that can be the same for a number of tests. These will be kept together, unless the other dependencies are used)
 * <p>
 * for each test log:
 * - commands and replies
 * - details, notes, warnings, errors and fatal errors (+ skips: the test can start to run and then decide it needs to be skipped)
 * - info on the test
 * - description of what it does
 * - description of what is expected
 */
public abstract class ApiTest {
    private final static org.slf4j.Logger LOG = LoggerFactory.getLogger(ApiTest.class);

    protected final Logger logger;
    protected final TargetAuthority testedAuthority;
    protected final GeniUserProvider geniUserProvider;
    protected final GeniUser user;

    protected final JFedConnectionProvider connectionProvider;
    protected final TestbedInfoSource testbedInfoSource;
    protected final AuthorityFinder authorityFinder;
    protected final JFedPreferences jFedPreferences;


    public Logger getLogger() {
        return logger;
    }

    public TargetAuthority getTestedAuthority() {
        return testedAuthority;
    }

    public GeniUser getUser() {
        return geniUserProvider.getLoggedInGeniUser();
    }

    public JFedConnectionProvider getConnectionProvider() {
        return connectionProvider;
    }

    public TestbedInfoSource getTestbedInfoSource() {
        return testbedInfoSource;
    }

    public AutomaticUserAndSliceApiWrapper.AutomaticUserAndSliceApiWrapperFactory getAutomaticUserAndSliceApiWrapperFactory() {
        return new AutomaticUserAndSliceApiWrapper.BasicAutomaticUserAndSliceApiWrapperFactory(logger, geniUserProvider, connectionProvider, jFedPreferences);
    }

    public AutomaticAggregateManagerWrapper.AutomaticAggregateManagerWrapperFactory getAutomaticAggregateManagerWrapperFactory() {
        return new AutomaticAggregateManagerWrapper.BasicAutomaticAggregateManagerWrapperFactory(logger, geniUserProvider, connectionProvider, jFedPreferences);
    }


    public ApiTest(Logger logger, TargetAuthority testedAuthority, GeniUserProvider geniUserProvider, JFedConnectionProvider connectionProvider,
                   TestbedInfoSource testbedInfoSource, JFedPreferences jFedPreferences) {
        this(logger, testedAuthority, geniUserProvider, connectionProvider, testbedInfoSource,
                new AuthorityFinder(testbedInfoSource, jFedPreferences) /*safe fallback as AuthorityFinder does not keep state*/,
                jFedPreferences);


    }

    public ApiTest(Logger logger, TargetAuthority testedAuthority, GeniUserProvider geniUserProvider, JFedConnectionProvider connectionProvider,
                   TestbedInfoSource testbedInfoSource, AuthorityFinder authorityFinder, JFedPreferences jFedPreferences) {
        this.logger = logger;
        this.testedAuthority = testedAuthority;
        this.geniUserProvider = geniUserProvider;
        if (geniUserProvider != null)
            this.user = geniUserProvider.getLoggedInGeniUser();
        else
            this.user = null;
        this.connectionProvider = connectionProvider;
        this.testbedInfoSource = testbedInfoSource;
        this.authorityFinder = authorityFinder;
        this.jFedPreferences = jFedPreferences;
    }

    private Properties testConfig;

    public Properties getTestConfig() {
        return testConfig;
    }

    void setTestConfig(Properties testConfig) {
        this.testConfig = testConfig;
    }

    /**
     * Overwrite to specify required config keys
     */
    protected List<String> getReqConfKeys() {
        return new ArrayList<>();
    }

    /**
     * Overwrite to specify optional config keys
     */
    protected List<String> getOptConfKeys() {
        return new ArrayList<>();
    }


    protected abstract String getTestDescription();

    public abstract void setUp() throws Exception;

    protected ApiTestResult.ApiTestMethodResult currentTestResult;

    void setCurrentTestResult(ApiTestResult.ApiTestMethodResult currentTestResult) {
        this.currentTestResult = currentTestResult;
    }

    ApiTestResult.ApiTestMethodResult getCurrentTestResult() {
        return currentTestResult;
    }

    public LogOutput.TestResultState getCurrentResultState() {
        return currentTestResult.getState();
    }

    public JFedPreferences getJFedPreferences() {
        return jFedPreferences;
    }

    //TODO Replace these 3 with a description and options defined using class annotations?
    public static String getDescription(Class<? extends ApiTest> testClass) {
        //bit of a hack, but it works with all null's ...
        ApiTestFactory apiTestFactory =
                new ApiTestFactory(null/*injector*/, null/*user*/, null/* connectionProvider*/, null/* testbedInfoSource*/, null/* jFedPreferences*/, null/*automaticAggregateManagerWrapperFactory*/);
        try {
            ApiTest test = apiTestFactory.createNullApiTest(testClass);
            return test.getTestDescription();
        } catch (ApiTestFactory.InvalidApiTestException e) {
            LOG.error("Error fetching test description", e);
            return "Error fetching test description";
        }
    }

    public static List<String> getRequiredConfigKeys(Class<? extends ApiTest> testClass) {
        ApiTestFactory apiTestFactory =
                new ApiTestFactory(null/*injector*/, null/*user*/, null/* connectionProvider*/, null/* testbedInfoSource*/, null/* jFedPreferences*/, null/*automaticAggregateManagerWrapperFactory*/);
        try {
            ApiTest test = apiTestFactory.createNullApiTest(testClass);
            return test.getReqConfKeys();
        } catch (ApiTestFactory.InvalidApiTestException e) {
            LOG.error("Error fetching test description", e);
            return new ArrayList<>();
        }
    }

    public static List<String> getOptionalConfigKeys(Class<? extends ApiTest> testClass) {
        ApiTestFactory apiTestFactory =
                new ApiTestFactory(null/*injector*/, null/*user*/, null/* connectionProvider*/, null/* testbedInfoSource*/, null/* jFedPreferences*/, null/*automaticAggregateManagerWrapperFactory*/);
        try {
            ApiTest test = apiTestFactory.createNullApiTest(testClass);
            return test.getOptConfKeys();
        } catch (ApiTestFactory.InvalidApiTestException e) {
            LOG.error("Error fetching test description", e);
            return new ArrayList<>();
        }
    }

    @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
    @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
    public @interface Test {
        java.lang.String description() default "";

        java.lang.String[] groups() default {};

        java.lang.String[] hardDepends() default {};

        java.lang.String[] softDepends() default {};

        String dataProvider() default ""; //TODO implement

        //this concept (isReturnValue annotation) is under revision
//        /**
//         * If true, only the success of this test will be used as return value for the entire test run
//         * If there are no tests with this option, the success of all tests is used.
//         * If there are multiple tests in a class with this option, the result is unspecified!
//         * */
//        boolean ignoreForReturnValue() default false;
    }

    public @interface DataProvider { //TODO implement
        String name() default "";
    }


    public void note(String val) {
        currentTestResult.addLogLine(LogOutput.LogLineType.NOTE, val);
    }

    public void note(String val, boolean formatAsCode) {
        currentTestResult.addLogLine(LogOutput.LogLineType.NOTE, val, formatAsCode);
    }

    public void note(String val, Throwable t) {
        currentTestResult.addLogLine(LogOutput.LogLineType.NOTE, val, t);
    }

    public void note(String val, Throwable t, boolean formatAsCode) {
        currentTestResult.addLogLine(LogOutput.LogLineType.NOTE, val, t, formatAsCode);
    }

    public void skip(String reason) {
        currentTestResult.state = ApiTestResult.TestResultState.SKIPPED;
//        currentTestResult.addLogLine(ApiTestResult.MethodLogLineType.NOTE, "Test skipped: "+reason);
        throw new AutomatedTestRunner.SkipTestException(reason);
    }

    public void skip(String reason, Throwable t) {
        currentTestResult.state = ApiTestResult.TestResultState.SKIPPED;
        throw new AutomatedTestRunner.SkipTestException(reason, t);
    }

    private boolean errorsFatal = true;
    private boolean errorsAreWarnings = false;

    /**
     * errors will no longer be fatal, until setErrorsFatal() is called, or the current annotated test is done (so next test methods wil have fatal errors again).
     */
    public void setErrorsNotFatal() {
        errorsFatal = false;
        errorsAreWarnings = false;
    }

    public void setErrorsFatal() {
        errorsFatal = true;
        errorsAreWarnings = false;
    }

    /**
     * Errors are no longer errors, and are not fatal. They are warnings instead.
     * This works until setErrorsAreWarnings(false), setErrorsNotFatal() or setErrorsFatal() is called, or until the current annotated test is done (so next test methods wil have fatal errors again).
     */
    public void setErrorsAreWarnings(boolean warn) {
        errorsAreWarnings = warn;
    }


    /**
     * Makes the test fail, but continues executing the test code.
     *
     * @param val text describing the error
     */
    public void errorNonFatal(String val) {
        currentTestResult.state = ApiTestResult.TestResultState.FAILED;
        currentTestResult.addLogLine(LogOutput.LogLineType.ERROR, val);
    }

    public void errorNonFatal(String val, Throwable t) {
        currentTestResult.state = ApiTestResult.TestResultState.FAILED;
        currentTestResult.addLogLine(LogOutput.LogLineType.ERROR, val, t);
    }

    public void errorFatal(String val) {
        fatalError(val);
    }

    public void errorFatal(String val, Throwable t) {
        fatalError(val, t);
    }

    public void fatalError(String val) {
        fatalError(val, null);
    }

    public void fatalError(String val, Throwable t) {
        if (errorsAreWarnings) {
            warn(val, t);
        } else {
            if (errorsFatal) {
//        currentTestResult.addLogLine(ApiTestResult.MethodLogLineType.FATAL_ERROR, val);
                currentTestResult.state = ApiTestResult.TestResultState.FAILED;
                throw new AssertionError(val, t);
            } else {
                errorNonFatal(val, t);
            }
        }
    }

    public void warn(String val, boolean formatAsCode) {
        currentTestResult.addLogLine(LogOutput.LogLineType.WARN, val, formatAsCode);
        if (currentTestResult.state == ApiTestResult.TestResultState.SUCCESS)
            currentTestResult.state = ApiTestResult.TestResultState.WARN;
    }

    public void warn(String val) {
        this.warn(val, false/*formatAsCode*/);
    }

    public void warn(String val, Throwable e, boolean formatAsCode) {
        currentTestResult.addLogLine(LogOutput.LogLineType.WARN, val, e);
        if (currentTestResult.state == ApiTestResult.TestResultState.SUCCESS)
            currentTestResult.state = ApiTestResult.TestResultState.WARN;
    }

    public void warn(String val, Throwable e) {
        this.warn(val, e, false/*formatAsCode*/);
    }

    public void warnIfNot(boolean v) {
        warnIfNot(v, "warnIfNot failed");
    }

    public void warnIfNot(boolean v, String failText) {
        if (!v)
            warn(failText);
    }

    public void errorNonFatalIfNot(boolean v) {
        errorNonFatalIfNot(v, "warnIfNot failed");
    }

    public void errorNonFatalIfNot(boolean v, String failText) {
        if (!v)
            errorNonFatal(failText);
    }

    public void assertTrue(boolean v) {
        assertTrue(v, "assertTrue failed");
    }

    public void assertTrue(boolean v, String failText) {
        if (!v)
            fatalError(failText);
    }


    public void assertFalse(boolean v) {
        assertFalse(v, "assertFalse failed");
    }

    public void assertFalse(boolean v, String failText) {
        if (v)
            fatalError(failText);
    }


    public void assertNotNull(Object o) {
        assertNotNull(o, "object is null");
    }

    public void assertNotNull(Object o, String failText) {
        if (o == null)
            fatalError(failText);
    }


    public void assertNonEmptyString(String s) {
        assertNonEmptyString(s, "string should not be empty");
    }

    public void assertNonEmptyString(String s, String failText) {
        if (s == null)
            fatalError(failText);
        if (s.trim().isEmpty())
            fatalError(failText);
    }

    public void assertNotEmpty(Collection c) {
        assertNotEmpty(c, "list is empty");
    }

    public void assertEmpty(Collection c) {
        assertEmpty(c, "list is not empty");
    }

    public void assertNotEmpty(Collection c, String failText) {
        if (c.isEmpty())
            fatalError(failText);
    }

    public void assertEmpty(Collection c, String failText) {
        if (!c.isEmpty())
            fatalError(failText);
    }


    public void assertNull(Object o) {
        assertNotNull(o, "object is null");
    }

    public void assertNull(Object o, String failText) {
        if (o != null)
            fatalError(failText);
    }

    public void assertEquals(Object a, Object b, String failText) {
        if (!Objects.equals(a, b))
            fatalError(failText + " -> " + (a instanceof String
                    ? "object are not equal: a=\"" + a.toString() + "\" b=\"" +
                    b.toString() + "\"" : "object are not equal: a=" + a.toString() + "b=" + b.toString()));
    }

    public void assertEquals(Object a, Object b) {
        assertEquals(a, b, a instanceof String ? "object are not equal: a=\"" + a.toString() + "\" b=\""
                + b.toString() + "\"" : "object are not equal: a=" + a.toString() + "b=" + b.toString());
    }

    /**
     * assert that 2 dates are equal, to second precision. sub seconds precision may differ
     */
    public void assertEqualDatesNoMs(Date a, Date b) {
        long as = a.getTime() / 1000L;
        long bs = b.getTime() / 1000L;
        LOG.debug("assertEqualDatesNoMs compare as=" + as + " bs=" + bs + "   a=" + a + "  b=" + b);
        if (as != bs)
            fatalError("dates are not equal: " + a.toString() + " differs from " + b.toString() + "");
    }


    public void assertNotEquals(Object a, Object b, String failText) {
        if (Objects.equals(a, b))
            fatalError(failText + " -> object are equal: a=b=" + a.toString());
    }

    public void assertNotEquals(Object a, Object b) {
        assertNotEquals(a, b, "object are equal: a=b=" + a.toString());
    }

    public <T> T assertInstanceOf(Object o, Class<T> c) {
        return assertInstanceOf(o, c, "object is not an instance of class \"" + c.getName() + "\". (It is instance of class " + c.getName() + ")");
    }

    public <T> T assertInstanceOf(Object o, Class<T> c, String failText) {
        if (!c.isInstance(o)) {
            fatalError(failText + " -> object is not an instance of class \"" + c.getName() + "\". (It is instance of class " + c.getName() + ")");
            return null;
        } else
            return c.cast(o);
    }


    public List assertMapContainsList(Map t, String key) {
        Object o = t.get(key);
        assertNotNull(o, "Map does not contain key \"" + key + "\"");
        if (o == null) return null;
        assertTrue(o instanceof Object[], "value for " + key + " is not a List but a " + o.getClass().getName());
        if ((o instanceof Object[])) {
            return Arrays.asList((Object[]) o);
        } else
            return null;
    }

    @SuppressWarnings("unchecked")
    public <T> List<T> assertMapContainsList(Map t, String key, Class<T> clazz) {
        List list = assertMapContainsList(t, key);
        for (Object o : list) {
            assertInstanceOf(o, clazz,
                    "Value in List was expected to be a " + clazz.getName() + ", but got a " + o.getClass().getName());
        }
        return (List<T>) list;
    }

    public Map assertMapContainsMap(Map t, String key) {
        Object o = t.get(key);
        assertNotNull(o, "Map does not contain key \"" + key + "\"");
        if (o == null) return null;
        assertInstanceOf(o, Map.class, "value for " + key + " is not a Map but a " + o.getClass().getName());
        if (!(o instanceof Map)) return null;
        return (Map) o;
    }

    public String assertMapContainsString(Map t, String key) {
        Object o = t.get(key);
        assertNotNull(o, "Map does not contain key \"" + key + "\"");
        if (o == null) return null;
        assertEquals(o.getClass(), String.class, "value for " + key + " is not a String but a " + o.getClass().getName());
        if (!Objects.equals(o.getClass(), String.class)) return null;
        return (String) o;
    }

    public String assertMapContainsString(Map t, String key, String expectedValue) {
        Object o = t.get(key);
        assertNotNull(o, "Map does not contain key \"" + key + "\"");
        if (o == null) return null;
        assertEquals(o.getClass(), String.class, "value for " + key + " is not a String but a " + o.getClass().getName());
        assertEquals(o, expectedValue, "value for " + key + " is not \"" + expectedValue + "\" but \"" + o + "\"");
        if (!Objects.equals(o.getClass(), String.class)) return null;
        return (String) o;
    }

    public Object assertMapContainsObject(Map t, String key) {
        Object o = t.get(key);
        assertNotNull(o, "Map does not contain key \"" + key + "\"");
        return o;
    }

    public String assertMapContainsNonemptyString(Map t, String key) {
        Object o = t.get(key);
        assertNotNull(o, "Map does not contain key \"" + key + "\"");
        if (o == null) return null;
        assertEquals(o.getClass(), String.class, "value for " + key + " is not a String but a " + o.getClass().getName());
        if (!Objects.equals(o.getClass(), String.class)) return null;
        String s = (String) o;
        assertTrue(s.length() > 0, "value for " + key + " is an empty string");
        return s;
    }


    @Nullable
    public Map<String, Object> assertStringObjectMap(Object o) {
        Map t =
                assertInstanceOf(o, Map.class, "Expected a dictionary (=Map), but is of type " + o.getClass().getName());
        if (t != null) {
            for (Object key : t.keySet()) {
                assertInstanceOf(key, String.class, "Expected dictionary to only contain Strings as keys, but got a " + key.getClass().getName());
            }

            //noinspection unchecked
            return (Map<String, Object>) t;
        } else
            return null;
    }

    /**
     * @param type is allowed to be null if no type to test
     */
    public void assertValidUrn(String urn, String type) {
        assertValidUrn(urn, type, null);

    }

    public void assertValidUrn(String urn, String type, String message) {
        String msgHelp = message == null ? "" : message + ": ";
        GeniUrn geniUrn = GeniUrn.parse(urn);
        assertTrue(geniUrn != null, msgHelp + "Urn is not valid: \"" + urn + "\"");
        if (geniUrn == null) return;
        if (type != null)
            assertEquals(geniUrn.getEncodedResourceType(), type, msgHelp + "Urn type is not correct in urn=\"" + urn + "\"");
        assertFalse(geniUrn.getEncodedTopLevelAuthority().isEmpty(), msgHelp + "Urn authority is empty in urn=\"" + urn + "\"");
        assertFalse(geniUrn.getEncodedResourceName().isEmpty(), msgHelp + "Urn resource name is empty in urn=\"" + urn + "\"");
    }

}

