import ceylon.test {
    ...
}
import ceylon.test.event {
    ...
}
import ceylon.test.annotation {
    ...
}
import ceylon.test.engine {
    AssertionComparisonError
}


"A [[TestListener]] that prints information about test execution to a given logging function,
 in [Test Anything Protocol v13](http://testanything.org/tap-version-13-specification.html) format.
 
 ### YAML keys used
 
 * `elapsed` for the [[elapsed time|TestResult.elapsedTime]], in milliseconds (not for skipped tests)
 * `reason` for the [[ignore reason|IgnoreAnnotation.reason]], if present
 * `severity` for the [[state|TestResult.state]], one of `failure` or `error` (omitted for successful tests)
 * `actual`, `expected` if the [[exception|TestResult.exception]] is an [[AssertionComparisonError]]
 * `exception` for the exception’s stack trace if it exists, but isn’t an [[AssertionComparisonError]].
 
 ### Example
 
 ~~~text
 TAP version 13
 1..4
 ok 1 - test.my.module::testFeature
   ---
   elapsed: 163
   ...
 not ok 2 - test.my.module::testOtherFeature
   ---
   elapsed: 11
   severity: failure
   actual: |
     Lorem ipsum dolor sit amet ,
     consetetur sadipscing elitr ,
     sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat ,
     sed diam voluptua .
   expected: |
     Lorem ipsum dolor sit amet,
     consetetur sadipscing elitr,
     sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
     sed diam voluptua.
   ...
 not ok 3 - test.my.module::testProposedFeature # SKIP skipped
   ---
   reason: not yet implemented
   ...
 not ok 4 - test.my.module::testBrokenFeature 
   ---
   elapsed: 15
   severity: error
   exception: |
     java.lang.Exception: Error
         at test.my.module.testBrokenFeature_.testBrokenFeature(testBrokenFeature.ceylon:3)
         at com.redhat.ceylon.compiler.java.runtime.metamodel.AppliedFunction.$call$(AppliedFunction.java:257)
         at com.redhat.ceylon.compiler.java.Util.apply(Util.java:934)
         at com.redhat.ceylon.compiler.java.runtime.metamodel.Metamodel.apply(Metamodel.java:1099)
         at com.redhat.ceylon.compiler.java.runtime.metamodel.AppliedFunction.apply(AppliedFunction.java:413)
         at com.redhat.ceylon.compiler.java.runtime.metamodel.FreeFunction.invoke(FreeFunction.java:262)
         at com.redhat.ceylon.compiler.java.runtime.metamodel.FreeFunction.invoke(FreeFunction.java:251)
         at com.redhat.ceylon.compiler.java.runtime.metamodel.FreeFunction.invoke(FreeFunction.java:244)
         at ceylon.test.internal.DefaultTestExecutor.invokeFunction$priv$(executors.ceylon:254)
         at ceylon.test.internal.DefaultTestExecutor.invokeTest$priv$(executors.ceylon:249)
         at ceylon.test.internal.DefaultTestExecutor.access$000(executors.ceylon:253)
         at ceylon.test.internal.DefaultTestExecutor$4.$call$(executors.ceylon)
         at ceylon.test.internal.DefaultTestExecutor$10.$call$(executors.ceylon:168)
         at ceylon.test.internal.DefaultTestExecutor$11.$call$(executors.ceylon:174)
         at ceylon.test.internal.DefaultTestExecutor$9.$call$(executors.ceylon:150)
         at ceylon.test.internal.DefaultTestExecutor$8.$call$(executors.ceylon:140)
         at ceylon.test.internal.DefaultTestExecutor$7.$call$(executors.ceylon:131)
         at ceylon.test.internal.DefaultTestExecutor$6.$call$(executors.ceylon:111)
         at ceylon.test.internal.DefaultTestExecutor$5.$call$(executors.ceylon:91)
         at ceylon.test.internal.DefaultTestExecutor.execute(executors.ceylon:61)
         at ceylon.test.internal.TestRunnerImpl.run(TestRunnerImpl.ceylon:49)
         at test.ceylon.formatter.run_.run(run.ceylon:4)
         at test.ceylon.formatter.run_.main(run.ceylon)
         at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
         at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
         at java.lang.reflect.Method.invoke(Method.java:606)
         at ceylon.modules.api.runtime.SecurityActions.invokeRunInternal(SecurityActions.java:58)
         at ceylon.modules.api.runtime.SecurityActions.invokeRun(SecurityActions.java:48)
         at ceylon.modules.api.runtime.AbstractRuntime.invokeRun(AbstractRuntime.java:85)
         at ceylon.modules.api.runtime.AbstractRuntime.execute(AbstractRuntime.java:145)
         at ceylon.modules.api.runtime.AbstractRuntime.execute(AbstractRuntime.java:129)
         at ceylon.modules.Main.execute(Main.java:69)
         at ceylon.modules.Main.main(Main.java:42)
         at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
         at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
         at java.lang.reflect.Method.invoke(Method.java:606)
         at org.jboss.modules.Module.run(Module.java:270)
         at org.jboss.modules.Main.main(Main.java:294)
         at ceylon.modules.bootstrap.CeylonRunTool.run(CeylonRunTool.java:208)
         at com.redhat.ceylon.common.tools.CeylonTool.run(CeylonTool.java:343)
         at com.redhat.ceylon.common.tools.CeylonTool.execute(CeylonTool.java:283)
         at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
         at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
         at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
         at java.lang.reflect.Method.invoke(Method.java:606)
         at com.redhat.ceylon.launcher.Launcher.run(Launcher.java:89)
         at com.redhat.ceylon.launcher.Launcher.main(Launcher.java:21)
   ...
 ~~~"
shared class TapReporter(write = print, close = noop) satisfies TestListener {
    
    "A function that logs the given line, for example [[print]]."
    void write(String line);
    "A function that is called at the end of reporting and may, for example, close an underlying stream."
    void close();
    
    variable Integer count = 1;
    
    shared actual void testRunStarted(TestRunStartedEvent event) {
        write("TAP version 13");
    }
    
    shared actual void testRunFinished(TestRunFinishedEvent event) {
        write("1..`` count - 1 ``");
        close();
    }
    
    shared actual void testFinished(TestFinishedEvent event) => writeProtocol(event);
    
    shared actual void testSkipped(TestSkippedEvent event) => writeProtocol(event);
    
    shared actual void testAborted(TestAbortedEvent event) => writeProtocol(event);
    
    shared actual void testError(TestErrorEvent event) => writeProtocol(event);
    
    void writeProtocol(TestFinishedEvent|TestSkippedEvent|TestAbortedEvent|TestErrorEvent event) {
        TestResult result;
        Throwable? exception;
        Integer? elapsed;
        String? reason;
        
        switch (event)
        case (is TestFinishedEvent) {
            result = event.result;
            exception = event.result.exception;
            elapsed = event.result.elapsedTime;
            reason = null;
        }
        case (is TestSkippedEvent) {
            result = event.result;
            reason = result.exception?.message;
            exception = null;
            elapsed = null;
        }
        case (is TestAbortedEvent) {
            result = event.result;
            reason = result.exception?.message;
            exception = null;
            elapsed = null;
        }
        case (is TestErrorEvent) {
            result = event.result;
            exception = event.result.exception;
            elapsed = null;
            reason = null;
        }
        
        String name = "``result.description.name````result.description.variant else ""``";
        String okOrNotOk = (result.state == TestState.success) then "ok" else "not ok";
        String directive = (result.state == TestState.skipped || result.state == TestState.aborted) then "# SKIP skipped" else "";
        String? severity = (result.state == TestState.failure) then "failure" else (result.state == TestState.error then "error");
        
        write("``okOrNotOk`` ``count`` - ``name`` ``directive``");
        
        if (elapsed exists || reason exists || exception exists) {
            write("  ---");
            if (exists elapsed) {
                write("  elapsed: ``elapsed``");
            }
            if (exists reason) {
                write("  reason: ``reason``");
            }
            if (exists severity) {
                write("  severity: ``severity``");
            }
            if (exists exception) {
                if (is AssertionComparisonError exception) {
                    write("  actual: |");
                    for (line in exception.actualValue.replace("\r\n", "\n").split('\n'.equals)) {
                        write("    ``line``");
                    }
                    write("  expected: |");
                    for (line in exception.expectedValue.replace("\r\n", "\n").split('\n'.equals)) {
                        write("    ``line``");
                    }
                } else {
                    write("  exception: |");
                    printStackTrace(exception, void(String string) {
                            for (line in string.replace("\r\n", "\n").split('\n'.equals).filter((String s) => !s.empty)) {
                                write("    ``line.replace("\t", "    ")``");
                            }
                        });
                }
            }
            write("  ...");
        }
        
        count++;
    }
}