package cucumber.runtime.formatter;

import cucumber.runtime.TestHelper;
import cucumber.runtime.model.CucumberFeature;
import gherkin.formatter.model.Result;
import org.custommonkey.xmlunit.Diff;
import org.custommonkey.xmlunit.Difference;
import org.custommonkey.xmlunit.XMLUnit;
import org.junit.Test;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.AbstractMap.SimpleEntry;

import static cucumber.runtime.TestHelper.result;
import static cucumber.runtime.Utils.toURL;
import static org.junit.Assert.assertTrue;

public final class TestNGFormatterTest {

    @Test
    public final void testScenarioWithUndefinedSteps() throws Throwable {
        CucumberFeature feature = TestHelper.feature("path/test.feature", "" +
                "Feature: feature\n" +
                "  Scenario: scenario\n" +
                "    When step\n" +
                "    Then step\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("step", result("undefined"));
        long stepDuration = milliSeconds(0);
        String actual = runFeatureWithTestNGFormatter(feature, stepsToResult, stepDuration);
        assertXmlEqual("" +
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                "<testng-results total=\"1\" passed=\"0\" failed=\"0\" skipped=\"1\">" +
                "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "            <class name=\"feature\">" +
                "                <test-method name=\"scenario\" status=\"SKIP\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\"/>" +
                "            </class>" +
                "        </test>" +
                "    </suite>" +
                "</testng-results>", actual);
    }

    @Test
    public void testScenarioWithUndefinedStepsStrict() throws Throwable {
        CucumberFeature feature = TestHelper.feature("path/test.feature", "" +
                "Feature: feature\n" +
                "  Scenario: scenario\n" +
                "    When step\n" +
                "    Then step\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("step", result("undefined"));
        long stepDuration = milliSeconds(0);
        String actual = runFeatureWithStrictTestNGFormatter(feature, stepsToResult, stepDuration);
        assertXmlEqual("" +
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                "<testng-results total=\"1\" passed=\"0\" failed=\"1\" skipped=\"0\">" +
                "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "            <class name=\"feature\">" +
                "                <test-method name=\"scenario\" status=\"FAIL\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\">" +
                "                    <exception class=\"The scenario has pending or undefined step(s)\">" +
                "                        <message><![CDATA[When step...................................................................undefined\n" +
                "Then step...................................................................undefined\n" +
                "]]></message>" +
                "                        <full-stacktrace><![CDATA[The scenario has pending or undefined step(s)]]></full-stacktrace>" +
                "                    </exception>" +
                "                </test-method>" +
                "            </class>" +
                "        </test>" +
                "    </suite>" +
                "</testng-results>", actual);
    }

    @Test
    public final void testScenarioWithPendingSteps() throws Throwable {
        CucumberFeature feature = TestHelper.feature("path/test.feature", "" +
                "Feature: feature\n" +
                "  Scenario: scenario\n" +
                "    When step1\n" +
                "    Then step2\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("step1", result("pending"));
        stepsToResult.put("step2", result("skipped"));
        long stepDuration = milliSeconds(0);
        String actual = runFeatureWithTestNGFormatter(feature, stepsToResult, stepDuration);
        assertXmlEqual("" +
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                "<testng-results total=\"1\" passed=\"0\" failed=\"0\" skipped=\"1\">" +
                "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "            <class name=\"feature\">" +
                "                <test-method name=\"scenario\" status=\"SKIP\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\"/>" +
                "            </class>" +
                "        </test>" +
                "    </suite>" +
                "</testng-results>", actual);
    }

    @Test
    public void testScenarioWithFailedSteps() throws Throwable {
        CucumberFeature feature = TestHelper.feature("path/test.feature", "" +
                "Feature: feature\n" +
                "  Scenario: scenario\n" +
                "    When step1\n" +
                "    Then step2\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("step1", result("failed", new TestNGException("message", "stacktrace")));
        stepsToResult.put("step2", result("skipped"));
        long stepDuration = milliSeconds(0);
        String actual = runFeatureWithTestNGFormatter(feature, stepsToResult, stepDuration);
        assertXmlEqual("" +
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                "<testng-results total=\"1\" passed=\"0\" failed=\"1\" skipped=\"0\">" +
                "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "            <class name=\"feature\">" +
                "                <test-method name=\"scenario\" status=\"FAIL\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\">" +
                "                    <exception class=\"cucumber.runtime.formatter.TestNGFormatterTest$TestNGException\">" +
                "                        <message><![CDATA[When step1..................................................................failed\n" +
                "Then step2..................................................................skipped\n" +
                "]]></message>" +
                "                        <full-stacktrace><![CDATA[stacktrace]]></full-stacktrace>" +
                "                    </exception>" +
                "                </test-method>" +
                "            </class>" +
                "        </test>" +
                "    </suite>" +
                "</testng-results>", actual);
    }

    @Test
    public final void testScenarioWithPassedSteps() throws Throwable {
        CucumberFeature feature = TestHelper.feature("path/test.feature", "" +
                "Feature: feature\n" +
                "  Scenario: scenario\n" +
                "    When step\n" +
                "    Then step\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("step", result("passed"));
        long stepDuration = milliSeconds(0);
        String actual = runFeatureWithTestNGFormatter(feature, stepsToResult, stepDuration);
        assertXmlEqual("" +
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                "<testng-results total=\"1\" passed=\"1\" failed=\"0\" skipped=\"0\">" +
                "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "            <class name=\"feature\">" +
                "                <test-method name=\"scenario\" status=\"PASS\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\"/>" +
                "            </class>" +
                "        </test>" +
                "    </suite>" +
                "</testng-results>", actual);
    }

    @Test
    public void testScenarioWithBackground() throws Throwable {
        CucumberFeature feature = TestHelper.feature("path/test.feature", "" +
                "Feature: feature\n" +
                "  Background:\n" +
                "    When background\n" +
                "    Then background\n" +
                "  Scenario: scenario\n" +
                "    When step\n" +
                "    Then step\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("background", result("undefined"));
        stepsToResult.put("step", result("undefined"));
        long stepDuration = milliSeconds(0);
        String actual = runFeatureWithTestNGFormatter(feature, stepsToResult, stepDuration);
        assertXmlEqual("" +
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                "<testng-results total=\"1\" passed=\"0\" failed=\"0\" skipped=\"1\">" +
                "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "            <class name=\"feature\">" +
                "                <test-method name=\"scenario\" status=\"SKIP\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\"/>" +
                "            </class>" +
                "        </test>" +
                "    </suite>" +
                "</testng-results>", actual);
    }

    @Test
    public void testScenarioOutlineWithExamples() throws Throwable {
        CucumberFeature feature = TestHelper.feature("path/test.feature", "" +
                "Feature: feature\n" +
                "  Scenario Outline: scenario\n" +
                "    When step\n" +
                "    Then step\n" +
                "    Examples:\n" +
                "    | arg |\n" +
                "    |  1  |\n" +
                "    |  2  |\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("step", result("undefined"));
        long stepDuration = milliSeconds(0);
        String actual = runFeatureWithTestNGFormatter(feature, stepsToResult, stepDuration);
        assertXmlEqual("" +
                    "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                    "<testng-results total=\"2\" passed=\"0\" failed=\"0\" skipped=\"2\">" +
                    "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                    "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                    "            <class name=\"feature\">" +
                    "                <test-method name=\"scenario\" status=\"SKIP\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\"/>" +
                    "                <test-method name=\"scenario_2\" status=\"SKIP\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\"/>" +
                    "            </class>" +
                    "        </test>" +
                    "    </suite>" +
                    "</testng-results>", actual);
    }

    @Test
    public void testDurationCalculationOfStepsAndHooks() throws Throwable {
        CucumberFeature feature1 = TestHelper.feature("path/feature1.feature", "" +
                "Feature: feature_1\n" +
                "  Scenario: scenario_1\n" +
                "    When step\n" +
                "    Then step\n" +
                "  Scenario: scenario_2\n" +
                "    When step\n" +
                "    Then step\n");
        CucumberFeature feature2 = TestHelper.feature("path/feature2.feature", "" +
                "Feature: feature_2\n" +
                "  Scenario: scenario_3\n" +
                "    When step\n" +
                "    Then step\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("step", result("passed"));
        List<SimpleEntry<String, Result>> hooks = new ArrayList<SimpleEntry<String, Result>>();
        hooks.add(TestHelper.hookEntry("before", result("passed")));
        hooks.add(TestHelper.hookEntry("after", result("passed")));
        long stepHookDuration = milliSeconds(1);
        String actual = runFeaturesWithTestNGFormatter(Arrays.asList(feature1, feature2), stepsToResult, hooks, stepHookDuration);
        assertXmlEqual("" +
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                "<testng-results total=\"3\" passed=\"3\" failed=\"0\" skipped=\"0\">" +
                "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"12\">" +
                "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"12\">" +
                "            <class name=\"feature_1\">" +
                "                <test-method name=\"scenario_1\" status=\"PASS\" duration-ms=\"4\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\"/>" +
                "                <test-method name=\"scenario_2\" status=\"PASS\" duration-ms=\"4\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\"/>" +
                "            </class>" +
                "            <class name=\"feature_2\">" +
                "                <test-method name=\"scenario_3\" status=\"PASS\" duration-ms=\"4\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\"/>" +
                "            </class>" +
                "        </test>" +
                "    </suite>" +
                "</testng-results>", actual);
    }

    @Test
    public void testScenarioWithFailedBeforeHook() throws Throwable {
        CucumberFeature feature = TestHelper.feature("path/test.feature", "" +
                "Feature: feature\n" +
                "  Scenario: scenario\n" +
                "    When step\n" +
                "    Then step\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("step", result("skipped"));
        List<SimpleEntry<String, Result>> hooks = new ArrayList<SimpleEntry<String, Result>>();
        hooks.add(TestHelper.hookEntry("before", result("failed", new TestNGException("message", "stacktrace"))));
        long stepHookDuration = milliSeconds(0);
        String actual = runFeatureWithTestNGFormatter(feature, stepsToResult, hooks, stepHookDuration);
        assertXmlEqual("" +
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                "<testng-results total=\"1\" passed=\"0\" failed=\"1\" skipped=\"0\">" +
                "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "            <class name=\"feature\">" +
                "                <test-method name=\"scenario\" status=\"FAIL\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\">" +
                "                    <exception class=\"cucumber.runtime.formatter.TestNGFormatterTest$TestNGException\">" +
                "                        <message><![CDATA[When step...................................................................skipped\n" +
                "Then step...................................................................skipped\n" +
                "]]></message>" +
                "                        <full-stacktrace><![CDATA[stacktrace]]></full-stacktrace>" +
                "                    </exception>" +
                "                </test-method>" +
                "            </class>" +
                "        </test>" +
                "    </suite>" +
                "</testng-results>", actual);
    }

    @Test
    public void testScenarioWithFailedAfterHook() throws Throwable {
        CucumberFeature feature = TestHelper.feature("path/test.feature", "" +
                "Feature: feature\n" +
                "  Scenario: scenario\n" +
                "    When step\n" +
                "    Then step\n");
        Map<String, Result> stepsToResult = new HashMap<String, Result>();
        stepsToResult.put("step", result("passed"));
        List<SimpleEntry<String, Result>> hooks = new ArrayList<SimpleEntry<String, Result>>();
        hooks.add(TestHelper.hookEntry("after", result("failed", new TestNGException("message", "stacktrace"))));
        long stepHookDuration = milliSeconds(0);
        String actual = runFeatureWithTestNGFormatter(feature, stepsToResult, hooks, stepHookDuration);
        assertXmlEqual("" +
                "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>" +
                "<testng-results total=\"1\" passed=\"0\" failed=\"1\" skipped=\"0\">" +
                "    <suite name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "        <test name=\"cucumber.runtime.formatter.TestNGFormatter\" duration-ms=\"0\">" +
                "            <class name=\"feature\">" +
                "                <test-method name=\"scenario\" status=\"FAIL\" duration-ms=\"0\" started-at=\"yyyy-MM-ddTHH:mm:ssZ\" finished-at=\"yyyy-MM-ddTHH:mm:ssZ\">" +
                "                    <exception class=\"cucumber.runtime.formatter.TestNGFormatterTest$TestNGException\">" +
                "                        <message><![CDATA[When step...................................................................passed\n" +
                "Then step...................................................................passed\n" +
                "]]></message>" +
                "                        <full-stacktrace><![CDATA[stacktrace]]></full-stacktrace>" +
                "                    </exception>" +
                "                </test-method>" +
                "            </class>" +
                "        </test>" +
                "    </suite>" +
                "</testng-results>", actual);
    }

    private String runFeatureWithTestNGFormatter(CucumberFeature feature, Map<String, Result> stepsToResult, long stepDuration)
            throws IOException, Throwable, FileNotFoundException {
        return runFeatureWithTestNGFormatter(feature, stepsToResult, Collections.<SimpleEntry<String, Result>>emptyList(), stepDuration);
    }

    private String runFeatureWithTestNGFormatter(CucumberFeature feature, Map<String, Result> stepsToResult,
            List<SimpleEntry<String, Result>> hooks, long stepDuration) throws IOException, Throwable, FileNotFoundException {
        return runFeaturesWithTestNGFormatter(Arrays.asList(feature), stepsToResult, hooks, stepDuration);
    }

    private String runFeaturesWithTestNGFormatter(List<CucumberFeature> features, Map<String, Result> stepsToResult,
            List<SimpleEntry<String, Result>> hooks, long stepDuration) throws IOException, Throwable, FileNotFoundException {
        final File tempFile = File.createTempFile("cucumber-jvm-testng", ".xml");
        final TestNGFormatter formatter = new TestNGFormatter(toURL(tempFile.getAbsolutePath()));
        TestHelper.runFeaturesWithFormatter(features, stepsToResult, hooks, stepDuration, formatter, formatter);
        return new Scanner(new FileInputStream(tempFile), "UTF-8").useDelimiter("\\A").next();
    }

    private String runFeatureWithStrictTestNGFormatter(CucumberFeature feature, Map<String, Result> stepsToResult, long stepDuration)
            throws IOException, Throwable, FileNotFoundException {
        final File tempFile = File.createTempFile("cucumber-jvm-testng", ".xml");
        final TestNGFormatter formatter = new TestNGFormatter(toURL(tempFile.getAbsolutePath()));
        formatter.setStrict(true);
        TestHelper.runFeatureWithFormatter(feature, stepsToResult, Collections.<SimpleEntry<String, Result>>emptyList(), stepDuration, formatter, formatter);
        return new Scanner(new FileInputStream(tempFile), "UTF-8").useDelimiter("\\A").next();
    }

    private void assertXmlEqual(String expected, String actual) throws SAXException, IOException {
        XMLUnit.setIgnoreWhitespace(true);
        Diff diff = new Diff(expected, actual) {
            @Override
            public int differenceFound(Difference difference) {
                if (difference.getControlNodeDetail().getNode().getNodeName().matches("started-at|finished-at")) {
                    return 0;
                }
                return super.differenceFound(difference);
            }
        };
        assertTrue("XML files are similar " + diff + "\nFormatterOutput = " + actual, diff.identical());
    }

    private Long milliSeconds(int milliSeconds) {
        return milliSeconds * 1000000L;
    }

    private static class TestNGException extends Exception {

        private final String stacktrace;

        public TestNGException(String message, String stacktrace) {
            super(message);
            this.stacktrace = stacktrace;
        }

        @Override
        public void printStackTrace(PrintWriter printWriter) {
            printWriter.print(stacktrace);
        }
    }
}
