#!/usr/bin/env python3
"""
A bowtie harness which supports being instrumented via a little mini-language.

Specifically, the string contents of test case descriptions and test
descriptions cause it to behave in various ways, so it can be used as a way to
test behavior (of Bowtie) by instrumenting a harness to behave accordingly in a
way which should not affect other participating harnesses (which don't or
shouldn't generally look at the description).

``envsonschema`` will claim to support all "real" JSON Schema dialects when
started.

For running test cases, it will follow a mini-language of instructions taken
from descriptions in both cases and individual tests (to guide either test case
behavior or test behavior accordingly).

Here's a short description of the minilanguage:

    * Words containing colons are instructions, with the right side of the
      colon containing comma-separated, ``=``-delimited arguments. For example,
      ``foo:bar=baz,quux=1`` is a ``foo`` instruction with ``bar`` and
      ``quux`` arguments (set to ``baz`` and ``1`` respectively)
    * Supported instructions (and signatures) for test cases are:
        - hang ():
            tell the harness that it should hang before exiting when it
            will be sent a ``stop`` request
        - crash (): crash the harness with an exception
        - sleep (number): sleep some number of seconds before responding
        - skip (**CaseSkipped without skipped)
            return a skipped result for the entire test case.
            Arguments are as in `CaseSkipped`, used to indicate skip reason.
        - error (**CaseErrored.context)
            return an error result for the entire test case
            Arguments are as in `CaseErrored`, used to indicate a message, etc.
        - split ()
            cause the response from this test case to be sent as multiple
            messages. Simulates a slow or split response from a harness.
    * Supported instructions (and signatures) for tests are:
        - valid (bool): explicitly set the validation result for this test
        - crash (): as above, for only this test
        - sleep (number) : as above, for only this test
        - skip (**SkippedTest.context) : as above, for only this test
        - error (**ErroredTest.context): as above, for only this test
        - annotate (**annotation)
            attach an annotation to this test's result, making it an
            annotations-format result. Arguments are the annotation's
            fields. May be repeated to attach multiple annotations.

Any other words are ignored.
"""

from dataclasses import dataclass
import io
import json
import shlex
import sys
import time


class Crash(Exception):
    """
    Self-immolation.
    """


_VALID = {"1": {"valid": True}, "0": {"valid": False}}


@dataclass
class Runner:
    _started: bool = False
    _stdout: io.TextIOWrapper = sys.stdout
    _hang_on_stop = False

    def run(self, stdin=sys.stdin):
        for line in stdin:
            each = json.loads(line)
            cmd = each.pop("cmd")
            getattr(self, f"cmd_{cmd}")(**each)

    def send(self, **response):
        self._stdout.write(f"{json.dumps(response)}\n")
        self._stdout.flush()

    def cmd_start(self, version):
        assert version == 1
        self._started = True
        self.send(
            version=1,
            implementation=dict(
                language="python",
                name="envsonschema",
                homepage="https://github.com/bowtie-json-schema/bowtie",
                issues="https://github.com/bowtie-json-schema/bowtie/issues",
                source="https://github.com/bowtie-json-schema/bowtie",
                dialects=[
                    "https://json-schema.org/draft/2020-12/schema",
                    "https://json-schema.org/draft/2019-09/schema",
                    "http://json-schema.org/draft-07/schema#",
                    "http://json-schema.org/draft-06/schema#",
                    "http://json-schema.org/draft-04/schema#",
                    "http://json-schema.org/draft-03/schema#",
                ],
            ),
        )

    def cmd_dialect(self, dialect):
        assert self._started, "Not started!"
        self.send(ok=True)

    def cmd_run(self, case, seq, output):
        assert self._started, "Not started!"

        send = self.send
        for instruction, arg in instructions(case["description"]):
            if instruction == "crash":
                raise Crash()
            elif instruction == "sleep":
                time.sleep(float(arg))
            elif instruction == "hang":
                self._hang_on_stop = True
            elif instruction == "error":
                result = dict(
                    errored=True,
                    seq=seq,
                    context=dict(kv.split("=", 1) for kv in arg.split(",")),
                )
                return self.send(**result)
            elif instruction == "skip":
                result = dict(
                    (each.split("=", 1) for each in arg.split(",")),
                    skipped=True,
                    seq=seq,
                )
                return self.send(**result)
            elif instruction == "split":

                def send(**response):
                    dumped = json.dumps(response)
                    size = len(dumped) // 2
                    self._stdout.write(dumped[:size])
                    self._stdout.flush()
                    time.sleep(0.1)
                    self._stdout.write(dumped[size:])
                    self._stdout.flush()
                    time.sleep(0.1)
                    self._stdout.write("\n")
                    self._stdout.flush()

        results = {"results": []}
        for test in case["tests"]:
            result = {"valid": False}
            annotations = []

            for instruction, arg in instructions(test["description"]):
                if instruction == "crash":
                    raise Crash()
                elif instruction == "sleep":
                    time.sleep(float(arg))

                if instruction == "valid":
                    result = _VALID.get(arg, {"valid": arg})
                elif instruction == "skip":
                    result = dict(
                        each.split("=", 1) for each in arg.split(",")
                    )
                    result.update(skipped=True)
                elif instruction == "error":
                    result = dict(
                        errored=True,
                        context=dict(
                            kv.split("=", 1) for kv in arg.split(",")
                        ),
                    )
                elif instruction == "annotate":
                    annotations.append(
                        dict(kv.split("=", 1) for kv in arg.split(",")),
                    )

            if annotations and "valid" in result:
                result = {**result, "annotations": annotations}

            results["results"].append(result)

        send(seq=seq, **results)

    def cmd_stop(self):
        assert self._started, "Not started!"
        if not self._hang_on_stop:
            sys.exit(0)


def instructions(description):
    words = (word.partition(":") for word in shlex.split(description))
    return ((instruction, arg) for instruction, colon, arg in words if colon)


Runner().run()
