import importlib
import sys
from pathlib import Path
from typing import Protocol

import pytest
from punq import InvalidForwardReferenceError

from iisi_app_core import (
    ComponentKind,
    ComponentRegistry,
    ContainerBuilder,
    PortKind,
    PortsAndAdaptersViolationError,
    application,
    component_definition_for,
    driven_adapter,
    driven_port,
    driving_adapter,
    driving_port,
    port_definition_for,
)
from iisi_app_core.bootstrap import autoload


def _create_pkg_file(path: Path, content: str = "") -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


def _component(target: type[object]):
    definition = component_definition_for(target)
    assert definition is not None
    return definition


def _registry(*targets: type[object]) -> ComponentRegistry:
    registry = ComponentRegistry()
    for target in targets:
        registry.add(_component(target))
    return registry


def test_driving_port_decorator_sets_metadata_for_protocols() -> None:
    @driving_port
    class UseCasePort(Protocol):
        def execute(self) -> str: ...

    definition = port_definition_for(UseCasePort)

    assert definition is not None
    assert definition.port is UseCasePort
    assert definition.kind is PortKind.DRIVING


def test_driven_port_decorator_sets_metadata_for_classes() -> None:
    @driven_port
    class UserRepository:
        def get(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    definition = port_definition_for(UserRepository)

    assert definition is not None
    assert definition.port is UserRepository
    assert definition.kind is PortKind.DRIVEN


def test_application_binds_declared_driving_port() -> None:
    @driving_port
    class GetUserProfilePort:
        def get_user_profile(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @application(port=GetUserProfilePort)
    class GetUserProfile(GetUserProfilePort):
        def get_user_profile(self, user_id: str) -> dict[str, str]:
            return {"id": user_id}

    definition = _component(GetUserProfile)

    assert definition.kind is ComponentKind.APPLICATION
    assert definition.interface is GetUserProfilePort
    assert definition.port is GetUserProfilePort


def test_driving_adapter_alias_sets_kind() -> None:
    @driving_adapter
    class DrivingAdapter:
        pass

    definition = _component(DrivingAdapter)

    assert definition.interface is DrivingAdapter
    assert definition.kind is ComponentKind.DRIVING_ADAPTER
    assert definition.port is None


def test_application_requires_explicit_driving_port() -> None:
    with pytest.raises(TypeError, match="application\\(\\) requires port=\\.\\.\\."):

        @application
        class UseCase:
            pass


def test_application_rejects_non_driving_port() -> None:
    @driven_port
    class UserRepository:
        def get(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    with pytest.raises(TypeError, match="@driving_port"):

        @application(port=UserRepository)
        class GetUserProfile(UserRepository):
            def get(self, user_id: str) -> dict[str, str]:
                return {"id": user_id}


def test_driven_adapter_rejects_non_driven_port() -> None:
    @driving_port
    class UseCasePort:
        def execute(self) -> str:
            raise NotImplementedError

    with pytest.raises(TypeError, match="@driven_port"):

        @driven_adapter(port=UseCasePort)
        class Adapter(UseCasePort):
            def execute(self) -> str:
                return "ok"


def test_component_registry_collects_loaded_modules_in_deterministic_order(tmp_path: Path) -> None:
    pkg_root = tmp_path / "samplepkg_registry"
    _create_pkg_file(pkg_root / "__init__.py")
    _create_pkg_file(
        pkg_root / "a.py",
        "from iisi_app_core import register\n@register\nclass Alpha:\n    pass\n",
    )
    _create_pkg_file(
        pkg_root / "b.py",
        "from iisi_app_core import register\n@register\nclass Beta:\n    pass\n",
    )

    sys.path.insert(0, str(tmp_path))
    try:
        package = importlib.import_module("samplepkg_registry")
        modules = autoload(package)
    finally:
        sys.path.remove(str(tmp_path))

    registry = ComponentRegistry.from_modules(modules)

    assert [definition.qualname for definition in registry.definitions()] == ["Alpha", "Beta"]


def test_container_builder_registers_declared_ports_and_concrete_types() -> None:
    @driven_port
    class UserRepository:
        def get(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @driven_adapter(port=UserRepository)
    class DynamoUserRepository(UserRepository):
        def get(self, user_id: str) -> dict[str, str]:
            return {"id": user_id, "name": "Ada"}

    @driving_port
    class GetUserProfilePort:
        def get_user_profile(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @application(port=GetUserProfilePort)
    class GetUserProfile(GetUserProfilePort):
        def __init__(self, repo: UserRepository) -> None:
            self.repo = repo

        def get_user_profile(self, user_id: str) -> dict[str, str]:
            return self.repo.get(user_id)

    @driving_adapter
    class GetUserProfileHandler:
        def __init__(self, app: GetUserProfilePort) -> None:
            self.app = app

        def handle(self, user_id: str) -> dict[str, str]:
            return self.app.get_user_profile(user_id)

    container = ContainerBuilder(_registry(DynamoUserRepository, GetUserProfile, GetUserProfileHandler)).build()

    assert isinstance(container.resolve(UserRepository), DynamoUserRepository)
    assert isinstance(container.resolve(DynamoUserRepository), DynamoUserRepository)
    assert isinstance(container.resolve(GetUserProfilePort), GetUserProfile)
    assert isinstance(container.resolve(GetUserProfile), GetUserProfile)
    assert container.resolve(GetUserProfileHandler).handle("123") == {"id": "123", "name": "Ada"}


def test_container_builder_rejects_driving_adapter_injecting_application_implementation() -> None:
    @driven_port
    class UserRepository:
        def get(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @driven_adapter(port=UserRepository)
    class DynamoUserRepository(UserRepository):
        def get(self, user_id: str) -> dict[str, str]:
            return {"id": user_id}

    @driving_port
    class GetUserProfilePort:
        def get_user_profile(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @application(port=GetUserProfilePort)
    class GetUserProfile(GetUserProfilePort):
        def __init__(self, repo: UserRepository) -> None:
            self.repo = repo

        def get_user_profile(self, user_id: str) -> dict[str, str]:
            return self.repo.get(user_id)

    @driving_adapter
    class GetUserProfileHandler:
        def __init__(self, app: GetUserProfile) -> None:
            self.app = app

    with pytest.raises(PortsAndAdaptersViolationError, match="decorated implementation '.*GetUserProfile'"):
        ContainerBuilder(_registry(DynamoUserRepository, GetUserProfile, GetUserProfileHandler)).build()


def test_container_builder_rejects_application_injecting_driven_adapter_implementation() -> None:
    @driven_port
    class UserRepository:
        def get(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @driven_adapter(port=UserRepository)
    class DynamoUserRepository(UserRepository):
        def get(self, user_id: str) -> dict[str, str]:
            return {"id": user_id}

    @driving_port
    class GetUserProfilePort:
        def get_user_profile(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @application(port=GetUserProfilePort)
    class GetUserProfile(GetUserProfilePort):
        def __init__(self, repo: DynamoUserRepository) -> None:
            self.repo = repo

        def get_user_profile(self, user_id: str) -> dict[str, str]:
            return self.repo.get(user_id)

    with pytest.raises(PortsAndAdaptersViolationError, match="decorated implementation '.*DynamoUserRepository'"):
        ContainerBuilder(_registry(DynamoUserRepository, GetUserProfile)).build()


def test_container_builder_rejects_application_injecting_list_of_driven_adapter_implementations() -> None:
    @driven_port
    class UserRepository:
        def get(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @driven_adapter(port=UserRepository)
    class DynamoUserRepository(UserRepository):
        def get(self, user_id: str) -> dict[str, str]:
            return {"id": user_id}

    @driving_port
    class GetUserProfilePort:
        def get_user_profile(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @application(port=GetUserProfilePort)
    class GetUserProfile(GetUserProfilePort):
        def __init__(self, repos: list[DynamoUserRepository]) -> None:
            self.repos = repos

        def get_user_profile(self, user_id: str) -> dict[str, str]:
            return self.repos[0].get(user_id)

    with pytest.raises(PortsAndAdaptersViolationError, match="decorated implementation '.*DynamoUserRepository'"):
        ContainerBuilder(_registry(DynamoUserRepository, GetUserProfile)).build()


def test_container_builder_rejects_application_bound_to_same_driving_port_twice() -> None:
    @driving_port
    class GetUserProfilePort:
        def get_user_profile(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @application(port=GetUserProfilePort)
    class GetUserProfile(GetUserProfilePort):
        def get_user_profile(self, user_id: str) -> dict[str, str]:
            return {"id": user_id}

    @application(port=GetUserProfilePort)
    class GetUserProfileCached(GetUserProfilePort):
        def get_user_profile(self, user_id: str) -> dict[str, str]:
            return {"id": user_id, "cached": "true"}

    with pytest.raises(PortsAndAdaptersViolationError, match="multiple Application implementations"):
        ContainerBuilder(_registry(GetUserProfile, GetUserProfileCached)).build()


def test_container_builder_rejects_driven_adapter_bound_to_same_driven_port_twice() -> None:
    @driven_port
    class UserRepository:
        def get(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @driven_adapter(port=UserRepository)
    class DynamoUserRepository(UserRepository):
        def get(self, user_id: str) -> dict[str, str]:
            return {"id": user_id}

    @driven_adapter(port=UserRepository)
    class CachedUserRepository(UserRepository):
        def get(self, user_id: str) -> dict[str, str]:
            return {"id": user_id, "cached": "true"}

    with pytest.raises(PortsAndAdaptersViolationError, match="multiple Driven adapter implementations"):
        ContainerBuilder(_registry(DynamoUserRepository, CachedUserRepository)).build()


def test_container_builder_rejects_driving_adapter_depends_on_driven_port() -> None:
    @driven_port
    class UserRepository:
        def get(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @driven_adapter(port=UserRepository)
    class DynamoUserRepository(UserRepository):
        def get(self, user_id: str) -> dict[str, str]:
            return {"id": user_id}

    @driving_adapter
    class InvalidHandler:
        def __init__(self, repo: UserRepository) -> None:
            self.repo = repo

    with pytest.raises(PortsAndAdaptersViolationError, match="Driving adapters may depend only on @driving_port ports"):
        ContainerBuilder(_registry(DynamoUserRepository, InvalidHandler)).build()


def test_container_builder_allows_undecorated_manual_dependencies() -> None:
    class Clock:
        def now(self) -> str:
            return "2026-03-07T12:00:00Z"

    @driven_port
    class UserRepository:
        def get(self, user_id: str) -> dict[str, str]:
            raise NotImplementedError

    @driven_adapter(port=UserRepository)
    class DynamoUserRepository(UserRepository):
        def __init__(self, clock: Clock) -> None:
            self.clock = clock

        def get(self, user_id: str) -> dict[str, str]:
            return {"id": user_id, "time": self.clock.now()}

    container = ContainerBuilder(_registry(DynamoUserRepository)).add_instance(Clock, Clock()).build()

    assert container.resolve(UserRepository).get("123") == {"id": "123", "time": "2026-03-07T12:00:00Z"}


def test_container_builder_preserves_punq_forward_reference_error_for_unresolved_annotations() -> None:
    @driving_port
    class UseCasePort:
        def execute(self) -> str:
            raise NotImplementedError

    @application(port=UseCasePort)
    class UseCase(UseCasePort):
        def __init__(self, missing: "MissingType") -> None:  # type: ignore[name-defined]  # noqa: F821
            self.missing = missing

        def execute(self) -> str:
            return "ok"

    with pytest.raises(InvalidForwardReferenceError, match="MissingType"):
        ContainerBuilder(_registry(UseCase)).build()
