/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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
 *
 *      https://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 org.apache.tools.ant.taskdefs.optional.junitlauncher.confined;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.ForkDefinition.ForkMode;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.Resources;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.Constants.LD_XML_ATTR_CLASS_NAME;
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.Constants.LD_XML_ATTR_EXCLUDE_ENGINES;
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.Constants.LD_XML_ATTR_HALT_ON_FAILURE;
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.Constants.LD_XML_ATTR_INCLUDE_ENGINES;
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.Constants.LD_XML_ATTR_OUTPUT_DIRECTORY;
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.Constants.LD_XML_ELM_TEST;
import static org.apache.tools.ant.taskdefs.optional.junitlauncher.confined.Constants.LD_XML_ELM_TEST_CLASSES;

/**
 * Represents a {@code testclasses} that's configured to be launched by the {@link JUnitLauncherTask}
 */
public class TestClasses extends TestDefinition {

    private final Resources resources = new Resources();

    public TestClasses() {

    }

    public void add(final ResourceCollection resourceCollection) {
        this.resources.add(resourceCollection);
    }

    public List<String> getTestClassNames() {
        if (this.resources.isEmpty()) {
            return Collections.emptyList();
        }
        final List<String> tests = new ArrayList<>();
        for (final Resource resource : resources) {
            if (!resource.isExists()) {
                continue;
            }
            final String name = resource.getName();
            // we only consider .class files
            if (!name.endsWith(".class")) {
                continue;
            }
            final String className = name.substring(0, name.lastIndexOf('.'));
            tests.add(className.replace(File.separatorChar, '.').replace('/', '.').replace('\\', '.'));
        }
        return tests;
    }

    @Override
    protected List<ForkedRepresentation> toForkedRepresentations() throws IllegalStateException {
        if (this.forkDefinition == null) {
            throw new IllegalStateException("tests haven't been configured to run in forked JVM");
        }
        final ForkMode forkMode = this.forkDefinition.getForkMode();
        if (forkMode == null) {
            // launch all test classes in a single forked JVM
            return Collections.singletonList(new ForkModeRep(this.getTestClassNames()));
        }
        switch (forkMode.getValue()) {
            case ForkMode.FORK_EVERY_TEST_CLASS: {
                // launch each test class in a separate forked JVM
                final List<ForkedRepresentation> forkModeReps = new ArrayList<>();
                for (final String testClass : getTestClassNames()) {
                    forkModeReps.add(new ForkModeRep(Collections.singletonList(testClass)));
                }
                return Collections.unmodifiableList(forkModeReps);
            }
            default: {
                throw new BuildException("Unsupported fork mode: " + forkMode.getValue());
            }
        }
    }

    private final class ForkModeRep extends ForkedRepresentation {
        private final List<String> testClassesToFork;

        private ForkModeRep(final List<String> testClassesToFork) {
            this.testClassesToFork = testClassesToFork;
        }

        @Override
        public void write(final JUnitLauncherTask task, final XMLStreamWriter writer)
                throws XMLStreamException {
            writer.writeStartElement(LD_XML_ELM_TEST_CLASSES);
            // write out each test class
            for (final String test : this.testClassesToFork) {
                writer.writeStartElement(LD_XML_ELM_TEST);
                writer.writeAttribute(LD_XML_ATTR_CLASS_NAME, test);
                if (haltOnFailure != null) {
                    writer.writeAttribute(LD_XML_ATTR_HALT_ON_FAILURE, haltOnFailure.toString());
                }
                if (outputDir != null) {
                    writer.writeAttribute(LD_XML_ATTR_OUTPUT_DIRECTORY, outputDir.getPath());
                }
                if (includeEngines != null) {
                    writer.writeAttribute(LD_XML_ATTR_INCLUDE_ENGINES, includeEngines);
                }
                if (excludeEngines != null) {
                    writer.writeAttribute(LD_XML_ATTR_EXCLUDE_ENGINES, excludeEngines);
                }
                // listeners for this test
                if (listeners != null) {
                    for (final ListenerDefinition listenerDef : getListeners()) {
                        if (!listenerDef.shouldUse(task.getProject())) {
                            // not applicable
                            continue;
                        }
                        listenerDef.toForkedRepresentation(writer);
                    }
                }
                writer.writeEndElement();
            }
            writer.writeEndElement();
        }
    }

    public static List<TestDefinition> fromForkedRepresentation(final XMLStreamReader reader) throws XMLStreamException {
        reader.require(XMLStreamConstants.START_ELEMENT, null, LD_XML_ELM_TEST_CLASSES);
        final List<TestDefinition> testDefinitions = new ArrayList<>();
        // read out as multiple SingleTestClass representations
        while (reader.nextTag() != XMLStreamConstants.END_ELEMENT) {
            final SingleTestClass testDefinition = new SingleTestClass();
            reader.require(XMLStreamConstants.START_ELEMENT, null, Constants.LD_XML_ELM_TEST);
            final String testClassName = requireAttributeValue(reader, LD_XML_ATTR_CLASS_NAME);
            testDefinition.setName(testClassName);
            final String halt = reader.getAttributeValue(null, LD_XML_ATTR_HALT_ON_FAILURE);
            if (halt != null) {
                testDefinition.setHaltOnFailure(Boolean.parseBoolean(halt));
            }
            final String outDir = reader.getAttributeValue(null, LD_XML_ATTR_OUTPUT_DIRECTORY);
            if (outDir != null) {
                testDefinition.setOutputDir(new File(outDir));
            }
            final String includeEngs = reader.getAttributeValue(null, LD_XML_ATTR_INCLUDE_ENGINES);
            if (includeEngs != null) {
                testDefinition.setIncludeEngines(includeEngs);
            }
            final String excludeEngs = reader.getAttributeValue(null, LD_XML_ATTR_EXCLUDE_ENGINES);
            if (excludeEngs != null) {
                testDefinition.setExcludeEngines(excludeEngs);
            }
            while (reader.nextTag() != XMLStreamConstants.END_ELEMENT) {
                reader.require(XMLStreamConstants.START_ELEMENT, null, Constants.LD_XML_ELM_LISTENER);
                testDefinition.addConfiguredListener(ListenerDefinition.fromForkedRepresentation(reader));
            }
            reader.require(XMLStreamConstants.END_ELEMENT, null, Constants.LD_XML_ELM_TEST);
            testDefinitions.add(testDefinition);
        }
        reader.require(XMLStreamConstants.END_ELEMENT, null, LD_XML_ELM_TEST_CLASSES);
        return Collections.unmodifiableList(testDefinitions);
    }

    private static String requireAttributeValue(final XMLStreamReader reader, final String attrName) throws XMLStreamException {
        final String val = reader.getAttributeValue(null, attrName);
        if (val != null) {
            return val;
        }
        throw new XMLStreamException("Attribute " + attrName + " is missing at " + reader.getLocation());
    }
}
