/**
 * Copyright 2011-2021 Asakusa Framework Team.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.asakusafw.testdriver.core;

import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.asakusafw.testdriver.rule.VerifyRuleBuilder;
import com.asakusafw.vocabulary.external.ExporterDescription;

/**
 * Inspects each test result.
 * @since 0.2.0
 * @deprecated Use {@link TestModerator} instead
 */
@Deprecated
public class TestResultInspector {

    static final Logger LOG = LoggerFactory.getLogger(TestResultInspector.class);

    private final DataModelAdapter adapter;

    private final DataModelSourceProvider sources;

    private final VerifyRuleProvider rules;

    private final ExporterRetriever<ExporterDescription> targets;

    private final TestContext context;

    /**
     * Creates a new instance which uses registerd services.
     * @param serviceClassLoader class loader to load services
     * @throws IllegalArgumentException if some parameters were {@code null}
     */
    public TestResultInspector(ClassLoader serviceClassLoader) {
        this(new TestContext.Empty(), serviceClassLoader);
    }

    /**
     * Creates a new instance which uses the specified services.
     * @param adapter data model driver
     * @param sources test data provider
     * @param rules verification rule provider
     * @param retrievers test result retrievers
     * @throws IllegalArgumentException if some parameters were {@code null}
     */
    public TestResultInspector(
            DataModelAdapter adapter,
            DataModelSourceProvider sources,
            VerifyRuleProvider rules,
            ExporterRetriever<ExporterDescription> retrievers) {
        this(new TestContext.Empty(), adapter, sources, rules, retrievers);
    }

    /**
     * Creates a new instance which uses registerd services.
     * @param context the current context
     * @param serviceClassLoader class loader to load services
     * @throws IllegalArgumentException if some parameters were {@code null}
     * @since 0.2.2
     */
    public TestResultInspector(TestContext context, ClassLoader serviceClassLoader) {
        if (context == null) {
            throw new IllegalArgumentException("context must not be null"); //$NON-NLS-1$
        }
        if (serviceClassLoader == null) {
            throw new IllegalArgumentException("serviceClassLoader must not be null"); //$NON-NLS-1$
        }
        this.context = context;
        this.adapter = new SpiDataModelAdapter(serviceClassLoader);
        this.sources = new SpiDataModelSourceProvider(serviceClassLoader);
        this.rules = new SpiVerifyRuleProvider(serviceClassLoader);
        this.targets = new SpiExporterRetriever(serviceClassLoader);
    }

    /**
     * Creates a new instance which uses the specified services.
     * @param context the current context
     * @param adapter data model driver
     * @param sources test data provider
     * @param rules verification rule provider
     * @param retrievers test result retrievers
     * @throws IllegalArgumentException if some parameters were {@code null}
     * @since 0.2.2
     */
    public TestResultInspector(
            TestContext context,
            DataModelAdapter adapter,
            DataModelSourceProvider sources,
            VerifyRuleProvider rules,
            ExporterRetriever<ExporterDescription> retrievers) {
        if (adapter == null) {
            throw new IllegalArgumentException("adapter must not be null"); //$NON-NLS-1$
        }
        if (sources == null) {
            throw new IllegalArgumentException("sources must not be null"); //$NON-NLS-1$
        }
        if (rules == null) {
            throw new IllegalArgumentException("rules must not be null"); //$NON-NLS-1$
        }
        if (retrievers == null) {
            throw new IllegalArgumentException("retrievers must not be null"); //$NON-NLS-1$
        }
        this.context = context;
        this.adapter = adapter;
        this.sources = sources;
        this.rules = rules;
        this.targets = retrievers;
    }

    /**
     * Inspects the target exporter's output using specified expected data and rule.
     * @param modelClass class of data model
     * @param description target exporter
     * @param verifyContext current verification context
     * @param expected the expected data
     * @param rule the verification rule between expected and actual result
     * @return detected invalid differences
     * @throws IOException if failed to inspect the result
     * @throws IllegalArgumentException if some parameters were {@code null}
     */
    public List<Difference> inspect(
            Class<?> modelClass,
            ExporterDescription description,
            VerifyContext verifyContext,
            URI expected,
            URI rule) throws IOException {
        if (modelClass == null) {
            throw new IllegalArgumentException("modelClass must not be null"); //$NON-NLS-1$
        }
        if (description == null) {
            throw new IllegalArgumentException("description must not be null"); //$NON-NLS-1$
        }
        if (verifyContext == null) {
            throw new IllegalArgumentException("verifyContext must not be null"); //$NON-NLS-1$
        }
        if (expected == null) {
            throw new IllegalArgumentException("expected must not be null"); //$NON-NLS-1$
        }
        if (rule == null) {
            throw new IllegalArgumentException("rule must not be null"); //$NON-NLS-1$
        }
        DataModelDefinition<?> definition = findDefinition(modelClass);
        VerifyRule ruleDesc = findRule(definition, verifyContext, rule);
        return inspect(modelClass, description, expected, ruleDesc);
    }

    /**
     * Creates a {@link VerifyRuleBuilder} for the target model class.
     * @param modelClass target model class
     * @return created {@link VerifyRuleBuilder}
     * @throws IOException if failed to create
     * @throws IllegalArgumentException if some parameters were {@code null}
     */
    public VerifyRuleBuilder rule(Class<?> modelClass) throws IOException {
        if (modelClass == null) {
            throw new IllegalArgumentException("modelClass must not be null"); //$NON-NLS-1$
        }
        DataModelDefinition<?> definition = findDefinition(modelClass);
        return new VerifyRuleBuilder(definition);
    }

    /**
     * Converts {@link ModelVerifier} into {@link VerifyRule}.
     * @param <T> type of model
     * @param modelClass class of model
     * @param verifier target verifier
     * @return converted rule
     * @throws IOException if failed to convert
     * @throws IllegalArgumentException if some parameters were {@code null}
     */
    public <T> VerifyRule rule(Class<? extends T> modelClass, ModelVerifier<T> verifier) throws IOException {
        if (modelClass == null) {
            throw new IllegalArgumentException("modelClass must not be null"); //$NON-NLS-1$
        }
        if (verifier == null) {
            throw new IllegalArgumentException("verifier must not be null"); //$NON-NLS-1$
        }
        DataModelDefinition<? extends T> definition = findDefinition(modelClass);
        return new ModelVerifierDriver<>(verifier, definition);
    }

    /**
     * Inspects the target exporter's output using specified expected data and rule.
     * @param modelClass class of data model
     * @param description target exporter
     * @param expected the expected data
     * @param rule the verification rule between expected and actual result
     * @return detected invalid differences
     * @throws IOException if failed to inspect the result
     * @throws IllegalArgumentException if some parameters were {@code null}
     */
    public List<Difference> inspect(
            Class<?> modelClass,
            ExporterDescription description,
            URI expected,
            VerifyRule rule) throws IOException {
        if (modelClass == null) {
            throw new IllegalArgumentException("modelClass must not be null"); //$NON-NLS-1$
        }
        if (description == null) {
            throw new IllegalArgumentException("description must not be null"); //$NON-NLS-1$
        }
        if (expected == null) {
            throw new IllegalArgumentException("expected must not be null"); //$NON-NLS-1$
        }
        if (rule == null) {
            throw new IllegalArgumentException("rule must not be null"); //$NON-NLS-1$
        }
        DataModelDefinition<?> definition = findDefinition(modelClass);
        DataModelSource expectedDesc = findSource(definition, expected);
        VerifyEngine engine = buildVerifier(definition, rule, expectedDesc);
        List<Difference> results = inspect(definition, description, engine);
        return results;
    }

    private <T> DataModelDefinition<T> findDefinition(Class<T> modelClass) throws IOException {
        assert modelClass != null;
        DataModelDefinition<T> definition = adapter.get(modelClass);
        if (definition == null) {
            throw new IOException(MessageFormat.format(
                    Messages.getString("TestResultInspector.errorMissingDataModelDefinition"), //$NON-NLS-1$
                    modelClass.getName()));
        }
        return definition;
    }

    private <T> List<Difference> inspect(
            DataModelDefinition<T> definition,
            ExporterDescription description,
            VerifyEngine engine) throws IOException {
        assert definition != null;
        assert description != null;
        assert engine != null;
        List<Difference> results = new ArrayList<>();
        try (DataModelSource target = targets.createSource(definition, description, context)) {
            results.addAll(engine.inspectInput(target));
        }
        results.addAll(engine.inspectRest());
        return results;
    }

    private VerifyEngine buildVerifier(
            DataModelDefinition<?> definition,
            VerifyRule rule,
            DataModelSource expected) throws IOException {
        assert definition != null;
        assert rule != null;
        VerifyEngine engine = new VerifyEngine(rule);
        engine.addExpected(expected);
        return engine;
    }

    private DataModelSource findSource(DataModelDefinition<?> definition, URI uri) throws IOException {
        assert definition != null;
        assert uri != null;
        DataModelSource expected = sources.open(definition, uri, context);
        if (expected == null) {
            throw new IOException(MessageFormat.format(
                    Messages.getString("TestResultInspector.errorMissingDataModelSource"), //$NON-NLS-1$
                    uri));
        }
        return expected;
    }

    private VerifyRule findRule(
            DataModelDefinition<?> definition,
            VerifyContext verifyContext,
            URI ruleUri) throws IOException {
        assert definition != null;
        assert verifyContext != null;
        assert ruleUri != null;
        VerifyRule rule = rules.get(definition, verifyContext, ruleUri);
        if (rule == null) {
            throw new IOException(MessageFormat.format(
                    Messages.getString("TestResultInspector.errorMissingVerifyRule"), //$NON-NLS-1$
                    ruleUri));
        }
        return rule;
    }
}
