from __future__ import annotations

import unittest
from types import SimpleNamespace
from unittest.mock import patch

from app.application.users import passwords


class FakeUpdateResult:
    def __init__(self, *, acknowledged: bool = True, matched_count: int = 1) -> None:
        self.acknowledged = acknowledged
        self.matched_count = matched_count


class FakeUsersCollection:
    def __init__(
        self,
        *,
        documents: dict[str, dict] | None = None,
        update_result: FakeUpdateResult | None = None,
        update_exception: Exception | None = None,
    ) -> None:
        self.documents = dict(documents or {})
        self.update_result = update_result or FakeUpdateResult()
        self.update_exception = update_exception
        self.updates: list[tuple[dict, dict]] = []

    async def find_one(self, query: dict) -> dict | None:
        return self.documents.get(str(query.get("username")))

    async def update_one(self, query: dict, payload: dict) -> FakeUpdateResult:
        if self.update_exception is not None:
            raise self.update_exception
        self.updates.append((query, payload))
        return self.update_result


class PasswordServiceTest(unittest.IsolatedAsyncioTestCase):
    def test_validate_new_password_rejects_confirmation_mismatch(self) -> None:
        with self.assertRaises(passwords.PasswordConfirmationMismatchError):
            passwords.validate_new_password("nueva123", "distinta123")

    async def test_change_own_password_updates_hash(self) -> None:
        fake_users = FakeUsersCollection(documents={"auditor": {"username": "auditor", "password": "stored-hash"}})
        fake_db = SimpleNamespace(users=fake_users)

        def fake_verify(plain: str, hashed: str) -> bool:
            return (plain, hashed) == ("actual123", "stored-hash")

        with (
            patch.object(passwords, "db", fake_db),
            patch.object(passwords, "verify_password", side_effect=fake_verify),
            patch.object(passwords, "hash_password", return_value="new-hash"),
        ):
            await passwords.change_own_password("auditor", "actual123", "nueva123")

        self.assertEqual(
            fake_users.updates,
            [({"username": "auditor"}, {"$set": {"password": "new-hash"}})],
        )

    async def test_change_own_password_rejects_invalid_current_password(self) -> None:
        fake_users = FakeUsersCollection(documents={"auditor": {"username": "auditor", "password": "stored-hash"}})

        with (
            patch.object(passwords, "db", SimpleNamespace(users=fake_users)),
            patch.object(passwords, "verify_password", return_value=False),
            self.assertRaises(passwords.InvalidCurrentPasswordError),
        ):
            await passwords.change_own_password("auditor", "incorrecta", "nueva123")

    async def test_change_own_password_rejects_reused_password(self) -> None:
        fake_users = FakeUsersCollection(documents={"auditor": {"username": "auditor", "password": "stored-hash"}})

        def fake_verify(plain: str, hashed: str) -> bool:
            return (plain, hashed) in {
                ("actual123", "stored-hash"),
                ("repetida123", "stored-hash"),
            }

        with (
            patch.object(passwords, "db", SimpleNamespace(users=fake_users)),
            patch.object(passwords, "verify_password", side_effect=fake_verify),
            self.assertRaises(passwords.PasswordUnchangedError),
        ):
            await passwords.change_own_password("auditor", "actual123", "repetida123")

    async def test_reset_user_password_rejects_missing_user(self) -> None:
        fake_users = FakeUsersCollection(documents={})

        with patch.object(
            passwords,
            "db",
            SimpleNamespace(users=fake_users),
        ), self.assertRaises(passwords.UserNotFoundError):
            await passwords.reset_user_password("admin", "ausente", "nueva123")

    async def test_change_own_password_wraps_hash_errors(self) -> None:
        fake_users = FakeUsersCollection(documents={"auditor": {"username": "auditor", "password": "stored-hash"}})

        def fake_verify(plain: str, hashed: str) -> bool:
            return (plain, hashed) == ("actual123", "stored-hash")

        with (
            patch.object(passwords, "db", SimpleNamespace(users=fake_users)),
            patch.object(passwords, "verify_password", side_effect=fake_verify),
            patch.object(passwords, "hash_password", side_effect=RuntimeError("hash failed")),
            self.assertRaises(passwords.PasswordHashingError),
        ):
            await passwords.change_own_password("auditor", "actual123", "nueva123")

    async def test_change_own_password_wraps_update_errors(self) -> None:
        fake_users = FakeUsersCollection(
            documents={"auditor": {"username": "auditor", "password": "stored-hash"}},
            update_exception=RuntimeError("mongo failed"),
        )

        def fake_verify(plain: str, hashed: str) -> bool:
            return (plain, hashed) == ("actual123", "stored-hash")

        with (
            patch.object(passwords, "db", SimpleNamespace(users=fake_users)),
            patch.object(passwords, "verify_password", side_effect=fake_verify),
            patch.object(passwords, "hash_password", return_value="new-hash"),
            self.assertRaises(passwords.PasswordUpdateError),
        ):
            await passwords.change_own_password("auditor", "actual123", "nueva123")


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