from __future__ import annotations

import ast
import unittest
from pathlib import Path

from backend.application import (
    AUTHENTICATED_POST_HANDLERS,
    PUBLIC_POST_HANDLERS,
    RequestHandler,
)


APP_ROOT = Path(__file__).resolve().parents[1]


def class_methods(path: Path, class_name: str) -> set[str]:
    tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    owner = next(
        node
        for node in tree.body
        if isinstance(node, ast.ClassDef) and node.name == class_name
    )
    return {
        node.name
        for node in owner.body
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
    }


class HttpDispatchContractTests(unittest.TestCase):
    @staticmethod
    def handler(path: str, calls: list[str]) -> RequestHandler:
        handler = RequestHandler.__new__(RequestHandler)
        handler.path = path
        handler.require_auth = lambda: calls.append("auth") or True
        handler.require_csrf = lambda: calls.append("csrf") or True
        handler.require_access = lambda method, route: (
            calls.append(f"access:{method}:{route}") or True
        )
        return handler

    def test_named_handlers_are_real_registered_post_routes(self) -> None:
        all_handlers = {**PUBLIC_POST_HANDLERS, **AUTHENTICATED_POST_HANDLERS}
        self.assertEqual(
            set(PUBLIC_POST_HANDLERS) & set(AUTHENTICATED_POST_HANDLERS), set()
        )
        for path, handler_name in all_handlers.items():
            with self.subTest(path=path):
                route = RequestHandler.route_registry.resolve("POST", path)
                self.assertIsNotNone(route)
                self.assertTrue(callable(getattr(RequestHandler, handler_name)))
                expected_access = "public" if path in PUBLIC_POST_HANDLERS else None
                if expected_access:
                    self.assertEqual(route.access, expected_access)
                else:
                    self.assertNotEqual(route.access, "public")

    def test_public_post_dispatches_without_authentication(self) -> None:
        for path, handler_name in PUBLIC_POST_HANDLERS.items():
            with self.subTest(path=path):
                calls: list[str] = []
                handler = self.handler(path, calls)
                handler.require_auth = lambda: (_ for _ in ()).throw(
                    AssertionError("public route required authentication")
                )
                setattr(handler, handler_name, lambda: calls.append(handler_name))

                RequestHandler.do_POST(handler)

                self.assertEqual(calls, [handler_name])

    def test_authenticated_post_preserves_guard_order(self) -> None:
        for path, handler_name in AUTHENTICATED_POST_HANDLERS.items():
            with self.subTest(path=path):
                calls: list[str] = []
                handler = self.handler(path, calls)
                setattr(handler, handler_name, lambda: calls.append(handler_name))

                RequestHandler.do_POST(handler)

                self.assertEqual(
                    calls,
                    ["auth", "csrf", f"access:POST:{path}", handler_name],
                )

    def test_failed_access_never_dispatches_protected_handler(self) -> None:
        path, handler_name = next(iter(AUTHENTICATED_POST_HANDLERS.items()))
        calls: list[str] = []
        handler = self.handler(path, calls)
        handler.require_access = lambda method, route: (
            calls.append(f"access:{method}:{route}") or False
        )
        setattr(handler, handler_name, lambda: calls.append(handler_name))

        RequestHandler.do_POST(handler)

        self.assertEqual(calls, ["auth", "csrf", f"access:POST:{path}"])

    def test_application_composition_and_route_ownership_stay_narrow(self) -> None:
        application = APP_ROOT / "backend" / "application.py"
        dispatch = APP_ROOT / "backend" / "http" / "dispatch.py"
        self.assertLessEqual(len(application.read_text(encoding="utf-8").splitlines()), 220)
        self.assertLessEqual(len(dispatch.read_text(encoding="utf-8").splitlines()), 150)
        self.assertEqual(class_methods(application, "DashboardService"), {"__init__"})
        self.assertEqual(class_methods(application, "RequestHandler"), set())
        self.assertEqual(
            class_methods(dispatch, "ApplicationHttpDispatchMixin"),
            {"_dispatch_named_handler", "do_GET", "do_POST", "do_DELETE"},
        )

        expected_route_owners = {
            "accounts", "alerts", "auction", "dragon_tiger", "heaven", "market",
            "mentor", "pools", "popularity", "review", "rotation", "screener",
            "sentiment", "system", "themes",
        }
        route_files = {
            path.parent.name: path
            for path in (APP_ROOT / "backend" / "features").glob("*/routes.py")
        }
        self.assertEqual(set(route_files), expected_route_owners)
        for path in route_files.values():
            source = path.read_text(encoding="utf-8")
            self.assertLessEqual(len(source.splitlines()), 120, path.name)
            self.assertNotIn("backend.application", source)
            self.assertNotRegex(source, r"\bSERVICE\b")

    def test_application_service_methods_have_single_domain_owners(self) -> None:
        owners = (
            ("features/system/service.py", "SystemServiceMixin", 300),
            ("features/accounts/application.py", "AccountApplicationMixin", 100),
            ("jobs/service.py", "JobServiceMixin", 80),
        )
        claimed: set[str] = set()
        for relative, class_name, line_limit in owners:
            path = APP_ROOT / "backend" / relative
            methods = class_methods(path, class_name)
            self.assertTrue(claimed.isdisjoint(methods))
            claimed.update(methods)
            self.assertLessEqual(len(path.read_text(encoding="utf-8").splitlines()), line_limit)
        self.assertEqual(len(claimed), 27)


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