"""Unit tests for the AuthenticateUser use case using fakes (no DB, no FastAPI).

Async use case is driven via ``asyncio.run`` so no pytest-asyncio plugin is needed.
"""

from __future__ import annotations

import asyncio
import uuid

import pytest

from src.application.identity.dto import LoginCommand
from src.application.identity.use_cases.authenticate_user import AuthenticateUser
from src.domain.identity.entities import User
from src.domain.identity.errors import (
    EmailNotVerified,
    InvalidCredentials,
    InvalidInstitutionalEmail,
)
from src.shared_kernel.errors import RateLimited


class FakeUsers:
    def __init__(self, user: User | None = None, roles: list[str] | None = None) -> None:
        self._user = user
        self._roles = roles or []
        self.reconciled = False

    async def get_by_email(self, email: str) -> User | None:
        return self._user if (self._user and self._user.email == email) else None

    async def get_by_id(self, user_id):  # pragma: no cover - unused here
        return self._user

    async def roles_after_reconcile(self, user: User) -> list[str]:
        self.reconciled = True
        return self._roles


class FakeHasher:
    def __init__(self, ok: bool = True) -> None:
        self.ok = ok

    def hash(self, plain: str) -> str:
        return "h:" + plain

    def verify(self, plain: str, hashed: str) -> bool:
        return self.ok


class FakeTokens:
    def issue(self, user_id, email, roles, credential_version) -> str:
        return f"tok:{user_id}:{credential_version}:{','.join(roles)}"


class FakeRateLimiter:
    def __init__(self, allow: bool = True) -> None:
        self._allow = allow

    def allow(self, email: str, client_ip: str) -> bool:
        return self._allow


class FakeAudit:
    def __init__(self) -> None:
        self.events: list[tuple] = []

    async def login_succeeded(self, *, user_id, email, roles) -> None:
        self.events.append(("ok", email, tuple(roles)))

    async def login_failed(self, *, email, user_id, reason) -> None:
        self.events.append(("fail", email, reason))


def _user(**kw) -> User:
    base = dict(
        id=uuid.uuid4(),
        email="a@ump.edu.vn",
        full_name="Test",
        password_hash="x",
        email_verified=True,
        is_active=True,
        credential_version=2,
    )
    base.update(kw)
    return User(**base)


def _build(users: FakeUsers, hasher=None, rate_limiter=None, audit=None) -> tuple:
    audit = audit or FakeAudit()
    uc = AuthenticateUser(
        users=users,
        hasher=hasher or FakeHasher(ok=True),
        tokens=FakeTokens(),
        rate_limiter=rate_limiter or FakeRateLimiter(allow=True),
        audit=audit,
    )
    return uc, audit


def test_login_success_returns_token_and_reconciles_roles() -> None:
    users = FakeUsers(user=_user(), roles=["admin", "viewer"])
    uc, audit = _build(users)
    result = asyncio.run(uc.execute(LoginCommand("A@ump.edu.vn", "pw", "1.2.3.4")))
    assert result.access_token.endswith(":2:admin,viewer")
    assert result.roles == ["admin", "viewer"]
    assert users.reconciled is True
    assert audit.events == [("ok", "a@ump.edu.vn", ("admin", "viewer"))]


def test_wrong_password_raises_401_and_audits_failure() -> None:
    users = FakeUsers(user=_user())
    uc, audit = _build(users, hasher=FakeHasher(ok=False))
    with pytest.raises(InvalidCredentials) as exc:
        asyncio.run(uc.execute(LoginCommand("a@ump.edu.vn", "bad", "ip")))
    assert exc.value.message == "Email hoặc mật khẩu không đúng."
    assert audit.events == [("fail", "a@ump.edu.vn", None)]


def test_unknown_email_raises_401() -> None:
    uc, _ = _build(FakeUsers(user=None))
    with pytest.raises(InvalidCredentials):
        asyncio.run(uc.execute(LoginCommand("nobody@ump.edu.vn", "pw", "ip")))


def test_unverified_email_raises_403_with_reason() -> None:
    users = FakeUsers(user=_user(email_verified=False))
    uc, audit = _build(users)
    with pytest.raises(EmailNotVerified):
        asyncio.run(uc.execute(LoginCommand("a@ump.edu.vn", "pw", "ip")))
    assert audit.events == [("fail", "a@ump.edu.vn", "email_unverified")]


def test_rate_limited_raises_429_before_db() -> None:
    users = FakeUsers(user=_user())
    uc, _ = _build(users, rate_limiter=FakeRateLimiter(allow=False))
    with pytest.raises(RateLimited):
        asyncio.run(uc.execute(LoginCommand("a@ump.edu.vn", "pw", "ip")))


def test_non_institutional_email_rejected_before_lookup() -> None:
    users = FakeUsers(user=_user())
    uc, _ = _build(users)
    with pytest.raises(InvalidInstitutionalEmail):
        asyncio.run(uc.execute(LoginCommand("a@gmail.com", "pw", "ip")))
