"""
Hand crafted classes which should undoubtedly be autogenerated from the schema.
"""
from __future__ import annotations

from typing import Any, Callable, ClassVar
import json

import attrs

from bowtie import exceptions


@attrs.frozen
class Test:

    description: str
    instance: object
    valid: bool | None = None


@attrs.frozen
class TestCase:

    description: str
    schema: object
    tests: list[Test]
    comment: str | None = None
    registry: dict | None = None

    @classmethod
    def from_dict(cls, tests, **kwargs):
        kwargs["tests"] = [Test(**test) for test in tests]
        return cls(**kwargs)

    def without_expected_results(self):
        as_dict = {
            "tests": [
                attrs.asdict(test, filter=lambda k, _: k.name != "valid")
                for test in self.tests
            ],
        }
        as_dict.update(
            attrs.asdict(
                self,
                filter=lambda k, v: k.name != "tests"
                and (k.name not in {"comment", "registry"} or v is not None),
            ),
        )
        return as_dict


@attrs.frozen
class Started:

    implementation: dict
    ready: bool = attrs.field()
    version: int = attrs.field()

    @ready.validator  # type: ignore[reportGeneralTypeIssues]
    def _check_ready(self, _, ready):
        if not ready:
            raise exceptions.ImplementationNotReady()

    @version.validator  # type: ignore[reportGeneralTypeIssues]
    def _check_version(self, _, version):
        if version != 1:
            raise exceptions.VersionMismatch(expected=1, got=version)


def command(name, Response) -> Callable[[type], type]:

    request_schema = {"$ref": f"#/$defs/command/$defs/{name}"}
    response_schema = {"$ref": f"#/$defs/command/$defs/{name}/$defs/response"}

    def _command(cls):
        def to_request(self, validate):
            request = dict(cmd=name, **attrs.asdict(self))
            validate(instance=request, schema=request_schema)
            return request

        def from_response(self, response, validate):
            try:
                instance = json.loads(response)
            except json.JSONDecodeError as error:
                raise exceptions._ProtocolError(errors=[error])
            validate(instance=instance, schema=response_schema)
            return Response(**instance)

        cls.to_request = to_request
        cls.from_response = from_response
        return attrs.frozen(cls)

    return _command


@command(name="start", Response=Started)
class Start:

    version: int


START_V1 = Start(version=1)  # type: ignore


@attrs.frozen
class StartedDialect:

    ok: bool

    OK: ClassVar[StartedDialect]


StartedDialect.OK = StartedDialect(ok=True)


@command(name="dialect", Response=StartedDialect)
class Dialect:

    dialect: str


def _case_result(errored=False, skipped=False, **response):
    if errored:
        return lambda implementation, expected: CaseErrored(
            implementation=implementation,
            **response,
        )
    elif skipped:
        return lambda implementation, expected: CaseSkipped(
            implementation=implementation,
            **response,
        )
    return lambda implementation, expected: CaseResult.from_dict(
        response,
        implementation=implementation,
        expected=expected,
    )


@attrs.frozen
class TestResult:

    errored = False
    skipped = False

    valid: bool

    @classmethod
    def from_dict(cls, data) -> TestResult | SkippedTest | ErroredTest:
        if data.pop("skipped", False):
            return SkippedTest(**data)
        elif data.pop("errored", False):
            return ErroredTest(**data)
        return cls(valid=data["valid"])


@attrs.frozen
class SkippedTest:

    message: str | None = attrs.field(default=None)
    issue_url: str | None = attrs.field(default=None)

    errored = False
    skipped: bool = attrs.field(init=False, default=True)

    @property
    def reason(self) -> str:
        if self.message is not None:
            return self.message
        if self.issue_url is not None:
            return self.issue_url
        return "skipped"


@attrs.frozen
class ErroredTest:

    context: dict[str, Any] = attrs.field(factory=dict)

    errored: bool = attrs.field(init=False, default=True)
    skipped: bool = False

    @property
    def reason(self) -> str:
        message = self.context.get("message")
        if message:
            return message
        return "Encountered an error."


@attrs.frozen
class CaseResult:

    errored = False

    implementation: str
    seq: int
    results: list[TestResult | SkippedTest | ErroredTest]
    expected: list[bool | None]

    @classmethod
    def from_dict(cls, data, **kwargs):
        return cls(
            results=[TestResult.from_dict(t) for t in data.pop("results")],
            **data,
            **kwargs,
        )

    @property
    def failed(self):
        return any(failed for _, failed in self.compare())

    def report(self, reporter):
        reporter.got_results(self)

    def compare(self):
        for test, expected in zip(self.results, self.expected):
            failed = (
                not test.skipped
                and not test.errored
                and expected is not None
                and expected != test.valid  # type: ignore[reportGeneralTypeIssues]  # noqa: E501
            )
            yield test, failed


@attrs.frozen
class CaseErrored:

    errored = True

    implementation: str
    seq: int
    context: dict[str, Any]

    caught: bool = True

    def report(self, reporter):
        return reporter.errored(self)

    @classmethod
    def uncaught(cls, implementation, seq, **context):
        return cls(
            implementation=implementation,
            seq=seq,
            caught=False,
            context=context,
        )


@attrs.frozen
class CaseSkipped:

    errored = False

    implementation: str
    seq: int

    message: str | None = None
    issue_url: str | None = None
    skipped: bool = attrs.field(init=False, default=True)

    def report(self, reporter):
        return reporter.skipped(self)


@attrs.frozen
class Empty:
    """
    An implementation didn't send a response.
    """

    errored = True

    implementation: str

    def report(self, reporter):
        reporter.no_response(implementation=self.implementation)


@command(name="run", Response=_case_result)
class Run:

    seq: int
    case: dict


@command(name="stop", Response=lambda: None)
class Stop:
    pass


STOP = Stop()
