"""Password policy per workspace (app/crud/password_policy.py): the Super
Admin sets it, and every way a password is set follows it — registering,
changing your own, an administrator setting someone's, and the new password
asked for at sign-in when one expired or an administrator set it."""

import uuid
from datetime import datetime, timedelta, timezone

import pytest
from sqlalchemy import text

from app.core.auth import create_platform_admin_token, hash_password
from app.crud.password_policy import PasswordPolicy, problems, requirements
from app.models.identity import User
from app.models.platform_admin import PlatformAdmin
from tests.conftest import AsyncSessionLocal

STRICT = {
    "min_length": 10,
    "max_length": 64,
    "min_uppercase": 1,
    "min_lowercase": 1,
    "min_digits": 1,
    "min_special": 1,
    "disallow_spaces": True,
    "max_repeated": 2,
    "disallow_personal_info": True,
    "disallow_common": True,
    "history_count": 2,
    "change_after_admin_set": True,
}
GOOD = "Kerala#Bus2026"


def test_each_rule():
    p = PasswordPolicy(**STRICT)
    assert problems(p, GOOD, name="Asha Menon", email="asha@example.com") == []
    assert problems(p, "short1A!") == ["Use at least 10 characters."]
    found = problems(p, "alllowercase")
    assert "Include at least 1 upper case letter." in found and "Include at least 1 digit." in found and "Include at least 1 symbol." in found
    assert "Don't use spaces." in problems(p, "Kerala #Bus2026")
    assert "Don't repeat a character more than 2 times in a row." in problems(p, "Keraaala#Bus2026")
    assert "Don't use your name or email address in it." in problems(p, "Asha#Menon2026", name="Asha Menon")
    assert "Don't use your name or email address in it." in problems(p, "Xx#asha123zz", email="asha@example.com")
    assert any("commonly used" in m for m in problems(PasswordPolicy(disallow_common=True), "Password123!"))
    kinds = PasswordPolicy(min_character_types=3)
    assert problems(kinds, "abcdefgh12") and not problems(kinds, "abcdefgh1!")
    pattern = PasswordPolicy(custom_pattern=r"[A-Z]{2}\d{6}.*", custom_message="Start with your two-letter office code and six digits.")
    assert problems(pattern, "kerala-bus") == ["Start with your two-letter office code and six digits."]
    assert not problems(pattern, "KL123456-bus")
    assert requirements(p)[0] == "10 to 64 characters long"


def test_settings_are_checked():
    with pytest.raises(ValueError):
        PasswordPolicy(min_length=20, max_length=10)
    with pytest.raises(ValueError):
        PasswordPolicy(custom_pattern="([")
    with pytest.raises(ValueError):
        PasswordPolicy(max_length=8, min_uppercase=5, min_digits=5)


async def _super_admin() -> tuple[dict, uuid.UUID]:
    async with AsyncSessionLocal() as db, db.begin():
        admin = PlatformAdmin(name="Pytest Super", email=f"pytest-super-{uuid.uuid4().hex[:8]}@example.com", password_hash=hash_password("pytest-password-123"), status="active")
        db.add(admin)
        await db.flush()
        admin_id = admin.id
    return {"Authorization": f"Bearer {create_platform_admin_token(admin_id=admin_id)}"}, admin_id


async def _drop_admin(admin_id: uuid.UUID) -> None:
    async with AsyncSessionLocal() as db, db.begin():
        await db.execute(text("DELETE FROM platform_admins WHERE id = :id"), {"id": str(admin_id)})


async def _login(client, tid, email, password):
    return await client.post("/auth/login", json={"tenant_id": str(tid), "email": email, "password": password})


async def test_super_admin_sets_the_policy_and_it_is_followed(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    headers, admin_id = await _super_admin()
    try:
        # Defaults until set — and anyone can read them.
        public = await client.get("/auth/password-policy", params={"tenant_id": str(tid)})
        assert public.status_code == 200 and public.json()["min_length"] == 8

        bad = await client.put(f"/admin/tenants/{tid}/password-policy", headers=headers, json={**STRICT, "max_length": 6})
        assert bad.status_code == 422
        saved = await client.put(f"/admin/tenants/{tid}/password-policy", headers=headers, json=STRICT)
        assert saved.status_code == 200, saved.text
        assert "not one of your last 2 passwords" in saved.json()["requirements"]
        assert (await client.get(f"/admin/tenants/{tid}/password-policy", headers=headers)).json()["min_length"] == 10
        # A workspace admin can't set it themselves.
        assert (await client.put(f"/admin/tenants/{tid}/password-policy", headers=auth_headers, json=STRICT)).status_code in (401, 403)

        # A workspace admin creating a user: the policy decides.
        email = f"ravi-{uuid.uuid4().hex[:6]}@example.com"
        weak = await client.post("/users", headers=auth_headers, json={"name": "Ravi Kumar", "email": email, "password": "password1"})
        assert weak.status_code == 422
        assert weak.json()["detail"]["code"] == "password_policy" and len(weak.json()["detail"]["problems"]) >= 3
        created = await client.post("/users", headers=auth_headers, json={"name": "Ravi Kumar", "email": email, "password": GOOD, "status": "active"})
        assert created.status_code == 201, created.text

        # An administrator set it, so the first sign-in asks for a new one.
        first = await _login(client, tid, email, GOOD)
        assert first.status_code == 200, first.text
        step = first.json()
        assert step["access_token"] is None and step["verification_required"] == "password_change"
        assert "set by an administrator" in step["notice"]
        change = {"tenant_id": str(tid), "challenge_token": step["challenge_token"]}
        assert (await client.post("/auth/login/change-password", json={**change, "new_password": GOOD})).status_code == 422  # the same one
        assert (await client.post("/auth/login/change-password", json={**change, "new_password": "Ravi#Kumar2026"})).status_code == 422  # their name
        done = await client.post("/auth/login/change-password", json={**change, "new_password": "Tram#Line2026"})
        assert done.status_code == 200, done.text
        token = done.json()["access_token"]
        assert token

        # Changing it again: the last two can't come back.
        me = {"Authorization": f"Bearer {token}"}
        reuse = await client.post("/auth/password", headers=me, json={"current_password": "Tram#Line2026", "new_password": GOOD})
        assert reuse.status_code == 422 and "last 2 passwords" in reuse.json()["detail"]["message"]
        fresh = await client.post("/auth/password", headers=me, json={"current_password": "Tram#Line2026", "new_password": "Ferry#Dock2026"})
        assert fresh.status_code == 204, fresh.text
        assert (await _login(client, tid, email, "Ferry#Dock2026")).json()["access_token"]
    finally:
        await _drop_admin(admin_id)


async def test_expired_password_is_changed_at_sign_in(client, auth_headers, test_tenant):
    tid = test_tenant["tenant_id"]
    headers, admin_id = await _super_admin()
    try:
        await client.put(f"/admin/tenants/{tid}/password-policy", headers=headers, json={"expiry_days": 30, "min_age_hours": 24})
        email = f"old-{uuid.uuid4().hex[:6]}@example.com"
        created = await client.post("/users", headers=auth_headers, json={"name": "Old Timer", "email": email, "password": "first-password-1", "status": "active"})
        assert created.status_code == 201, created.text
        # Fresh: signs in normally; changing it again so soon isn't allowed.
        token = (await _login(client, tid, email, "first-password-1")).json()["access_token"]
        assert token
        soon = await client.post("/auth/password", headers={"Authorization": f"Bearer {token}"}, json={"current_password": "first-password-1", "new_password": "second-password-2"})
        assert soon.status_code == 422 and "changed recently" in soon.json()["detail"]["message"]

        async with AsyncSessionLocal() as db, db.begin():
            await db.execute(text("SELECT set_config('app.current_tenant_id', :v, true)"), {"v": str(tid)})
            user = await db.get(User, uuid.UUID(created.json()["id"]))
            user.password_changed_at = datetime.now(timezone.utc) - timedelta(days=31)
        step = (await _login(client, tid, email, "first-password-1")).json()
        assert step["verification_required"] == "password_change" and "more than 30 days old" in step["notice"]
        done = await client.post("/auth/login/change-password", json={"tenant_id": str(tid), "challenge_token": step["challenge_token"], "new_password": "second-password-2"})
        assert done.status_code == 200 and done.json()["access_token"]
    finally:
        await _drop_admin(admin_id)


async def test_registration_follows_the_policy(client, test_tenant):
    tid = test_tenant["tenant_id"]
    headers, admin_id = await _super_admin()
    try:
        await client.put(f"/admin/tenants/{tid}/password-policy", headers=headers, json={"min_length": 12, "min_digits": 2})
        base = {"tenant_id": str(tid), "name": "Meera", "email": f"meera-{uuid.uuid4().hex[:6]}@example.com", "organization_name": "Meera Traders", "phone": "+91 98470 12345"}
        short = await client.post("/auth/register", json={**base, "password": "abc1"})
        assert short.status_code == 422, short.text
        ok = await client.post("/auth/register", json={**base, "password": "long-enough-pass-42"})
        assert ok.status_code == 201, ok.text
    finally:
        await _drop_admin(admin_id)
