"""Adapter-Basisklasse: keine Ausführung ohne Freigabe; strukturell keine Parallelpfade."""
import ast
import tempfile
import unittest
from pathlib import Path

from aria_router.adapter_contract import AdapterResult, BackendAdapter, Outbound, ResultStatus
from aria_router.adapters import MockAdapter
from aria_router.config import ConfigurationError

from tests.helpers import AdapterHarness, GatewayTestCase

PACKAGE = Path(__file__).resolve().parents[1] / "aria_router"


class Recording(BackendAdapter):
    name = "codex"

    def __init__(self, verifier, result=None):
        super().__init__(verifier)
        self.transmits = 0
        self.result = result

    def _prepare(self, job):
        return Outbound(self.name, {"context": job.envelope.canonical_dict()})

    def _transmit(self, outbound, job, clearance):
        self.transmits += 1
        return self.result or AdapterResult(job.job_id, self.name, ResultStatus.SUCCESS, output="ok")


class AdapterBaseTests(unittest.TestCase):
    def setUp(self):
        self._tmp = tempfile.TemporaryDirectory()
        self.h = AdapterHarness(Path(self._tmp.name))
        self.job = self.h.job("#public hallo")

    def tearDown(self):
        self._tmp.cleanup()

    def test_subclass_cannot_override_execute(self):
        with self.assertRaises(TypeError):
            class Bypass(BackendAdapter):  # noqa: F841
                name = "codex"

                def execute(self, clearance, job):
                    return "ausgeführt ohne Prüfung"

    def test_subclass_cannot_override_outbound_digest(self):
        with self.assertRaises(TypeError):
            class Bypass(BackendAdapter):  # noqa: F841
                name = "codex"

                def outbound_digest(self, job):
                    return "0" * 64

    def test_subclass_cannot_override_init_subclass(self):
        with self.assertRaises(TypeError):
            class Bypass(BackendAdapter):  # noqa: F841
                name = "codex"

                def __init_subclass__(cls, **kwargs):
                    pass

    def test_execute_without_clearance_blocked(self):
        adapter = Recording(self.h.verifier)
        result = adapter.execute(None, self.job)
        self.assertEqual((result.status, result.error_code), (ResultStatus.BLOCKED, "MISSING_CLEARANCE"))
        self.assertEqual(adapter.transmits, 0)

    def test_adapter_requires_verifier(self):
        with self.assertRaises(TypeError):
            Recording(object())

    def test_subclass_skipping_super_init_blocked(self):
        class Lazy(BackendAdapter):
            name = "codex"

            def __init__(self):
                self.transmits = 0

            def _prepare(self, job):
                return Outbound(self.name, {})

            def _transmit(self, outbound, job, clearance):
                self.transmits += 1
                return AdapterResult(job.job_id, self.name, ResultStatus.SUCCESS, output="x")

        adapter = Lazy()
        result = adapter.execute(None, self.job)
        self.assertEqual(result.error_code, "NO_VERIFIER")
        self.assertEqual(adapter.transmits, 0)

    def test_transmit_exception_failed(self):
        adapter = MockAdapter(self.h.verifier, "codex", raise_error=True)
        result = adapter.execute(self.h.clear(adapter, self.job), self.job)
        self.assertEqual((result.status, result.error_code), (ResultStatus.FAILED, "ADAPTER_EXCEPTION"))

    def test_invalid_result_rejected(self):
        adapter = Recording(self.h.verifier, AdapterResult("other-job", "codex", ResultStatus.SUCCESS, output="x"))
        result = adapter.execute(self.h.clear(adapter, self.job), self.job)
        self.assertEqual(result.error_code, "INVALID_ADAPTER_RESULT")

    def test_empty_success_rejected(self):
        adapter = Recording(self.h.verifier, AdapterResult("job-1", "codex", ResultStatus.SUCCESS, output="  "))
        result = adapter.execute(self.h.clear(adapter, self.job), self.job)
        self.assertEqual(result.error_code, "EMPTY_RESPONSE")


class GatewayAdapterWiringTests(GatewayTestCase):
    def test_gateway_rejects_foreign_verifier(self):
        foreign = AdapterHarness(self.root / "foreign")
        with self.assertRaises(ConfigurationError) as ctx:
            self.gateway(adapter_factories={"codex": lambda verifier: MockAdapter(foreign.verifier, "codex")})
        self.assertEqual(str(ctx.exception), "ADAPTER_VERIFIER_MISMATCH")

    def test_gateway_rejects_non_adapter(self):
        with self.assertRaises(ConfigurationError) as ctx:
            self.gateway(adapter_factories={"codex": lambda verifier: object()})
        self.assertEqual(str(ctx.exception), "INVALID_ADAPTER")


class StructuralTests(unittest.TestCase):
    """Keine parallelen Ausführungspfade (statische Prüfung des Pakets)."""

    def modules(self):
        for path in sorted(PACKAGE.glob("*.py")):
            yield path.name, ast.parse(path.read_text(encoding="utf-8"))

    def test_subprocess_only_in_runner(self):
        offenders = []
        for name, tree in self.modules():
            for node in ast.walk(tree):
                if isinstance(node, (ast.Import, ast.ImportFrom)):
                    names = [a.name for a in node.names] + ([node.module] if isinstance(node, ast.ImportFrom) else [])
                    if any(n and n.split(".")[0] in ("subprocess", "pty") for n in names) and name != "runner.py":
                        offenders.append(name)
                if isinstance(node, ast.Attribute) and node.attr in ("system", "popen", "execv", "execve", "spawnv"):
                    offenders.append(f"{name}:{node.attr}")
        self.assertEqual(offenders, [])

    def test_adapter_execute_called_only_by_gateway(self):
        callers = set()
        for name, tree in self.modules():
            for node in ast.walk(tree):
                if (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)
                        and node.func.attr in ("execute", "_transmit")):
                    callers.add((name, node.func.attr))
        self.assertEqual(callers, {("gateway.py", "execute"), ("adapter_contract.py", "_transmit")})

    def test_no_model_cli_strings_outside_codex_template(self):
        offenders = []
        for name, tree in self.modules():
            for node in ast.walk(tree):
                if isinstance(node, ast.Constant) and isinstance(node.value, str):
                    value = node.value
                    # Absolute Binärpfade (mit Verzeichnis), nicht der Telegram-Befehl "/claude".
                    if value.startswith("/") and value.count("/") >= 2 and value.endswith(("/claude", "/node")):
                        offenders.append(f"{name}:{value}")
                    if value in ("claude", "-p") and name not in ("core.py", "config.py", "gateway.py",
                                                                    "claude_channel.py", "commands.py"):
                        offenders.append(f"{name}:{value}")
        self.assertEqual(offenders, [])


if __name__ == "__main__":
    unittest.main()
