"""The local directory: principals are created once, refusals write nothing, mail is a seam.

`invite_user` and `create_user_with_password` against an existing username of any status,
or an existing email under another username, raise `AccountsError('EXISTS')` and change no
credential or token row; without a mail provider `invite_user` raises before writing
anything; `search_users` prefix-matches active accounts. In-process on SQLite.
"""

import pytest
from sqlalchemy import func, select

from app.models import Account, AccountCredential, AccountToken
from app.services import directory, mail
from app.vendor.bw_accounts import AccountsError
from tests import _accounts as T
from tests.kit import _env


@pytest.fixture
def world(monkeypatch):
    recorder = T.fresh(monkeypatch)
    T.person("rian")
    T.person("adam", level="admin")
    T.person("newbie", password=None, status="invited")
    T.person("gone", status="disabled")
    yield recorder


def counts() -> tuple[int, int, int]:
    with _env.TestSessionLocal() as db:
        return (
            db.scalar(select(func.count()).select_from(Account)),
            db.scalar(select(func.count()).select_from(AccountCredential)),
            db.scalar(select(func.count()).select_from(AccountToken)),
        )


class TestExists:
    @pytest.mark.parametrize("username", ["adam", "newbie", "gone", "RIAN"])
    def test_invite_refuses_an_existing_username_of_any_status(self, world, username):
        before = counts()
        with pytest.raises(AccountsError) as exc:
            directory.LocalDirectory().invite_user(username, "fresh@example.com")
        assert exc.value.code == "EXISTS"
        assert counts() == before and world.sent == []

    def test_invite_refuses_an_email_held_by_another_account(self, world):
        before = counts()
        with pytest.raises(AccountsError) as exc:
            directory.LocalDirectory().invite_user("someone", "ADAM@example.com")
        assert exc.value.code == "EXISTS"
        assert counts() == before and world.sent == []

    @pytest.mark.parametrize("username", ["adam", "newbie", "gone"])
    def test_password_delivery_refuses_an_existing_username(self, world, username):
        before = counts()
        with pytest.raises(AccountsError) as exc:
            directory.LocalDirectory().create_user_with_password(username, "fresh@example.com")
        assert exc.value.code == "EXISTS"
        assert counts() == before

    def test_a_bad_username_is_refused_before_anything_exists(self, world):
        with pytest.raises(AccountsError) as exc:
            directory.LocalDirectory().create_user_with_password("Not Valid!", "x@example.com")
        assert exc.value.code == "BAD_INPUT"


class TestHappyPaths:
    def test_invite_creates_an_invited_principal_and_mails_a_fragment_link(self, world):
        out = directory.LocalDirectory().invite_user("kristine", "Kristine@Example.com", "Kristine", "S")
        assert out["created"] and out["emailed_to"] == "kristine@example.com"
        with _env.TestSessionLocal() as db:
            account = db.scalar(select(Account).where(Account.username == "kristine"))
            assert account.status == "invited" and account.display_name == "Kristine S"
            assert db.get(AccountCredential, account.id) is None
            token = db.scalar(select(AccountToken).where(AccountToken.account_id == account.id))
        assert token.mode == "invite" and token.used_at is None
        (to, subject, text), = world.sent
        assert to == "kristine@example.com" and "/welcome#" in text
        assert "?" not in text and "/welcome/" not in text

    def test_password_delivery_creates_an_active_principal_with_a_must_change_credential(self, world):
        out = directory.LocalDirectory().create_user_with_password("matt", "matt@example.com")
        assert len(out["password"]) == 19 and world.sent == []
        with _env.TestSessionLocal() as db:
            account = db.scalar(select(Account).where(Account.username == "matt"))
            cred = db.get(AccountCredential, account.id)
        assert account.status == "active" and cred.must_change_password and out["password"] not in cred.password_hash

    def test_generated_reset_revokes_every_session(self, world):
        c = T.client()
        T.login(c, "adam")
        out = directory.LocalDirectory().reset_password_generated("adam")
        assert out["password"]
        assert c.post("/api/auth/heartbeat").status_code == 401
        assert T.login(T.client(), "adam", out["password"]).json()["must_change_password"] is True

    def test_userinfo_raises_for_absent_and_names_a_disabled_status(self, world):
        d = directory.LocalDirectory()
        assert d.userinfo("adam")["email"] == "adam@example.com"
        assert d.userinfo("gone")["status"] == "disabled"  # so the invite path answers DISABLED, not EXISTS
        with pytest.raises(LookupError):
            d.userinfo("nobody")

    def test_search_is_a_prefix_match_over_active_accounts(self, world):
        T.person("alice", level=None)
        T.person("aline", level=None)
        names = [u["username"] for u in directory.LocalDirectory().search_users("al")]
        assert names == ["alice", "aline"]
        assert [u["username"] for u in directory.LocalDirectory().search_users("go")] == []  # disabled


class TestMailSeam:
    def test_without_a_provider_invite_raises_before_writing(self, world, monkeypatch):
        monkeypatch.setattr(mail, "_override", None)
        monkeypatch.setattr(mail.settings, "mail_provider", "none")
        before = counts()
        with pytest.raises(mail.MailNotConfigured):
            directory.LocalDirectory().invite_user("nobody-yet", "nobody@example.com")
        assert counts() == before

    def test_send_reset_and_notify_go_to_the_stored_address_only(self, world):
        d = directory.LocalDirectory()
        d.send_reset("adam")
        d.notify_added("adam")
        assert [to for to, _, _ in world.sent] == ["adam@example.com", "adam@example.com"]
        with pytest.raises(LookupError):
            d.send_reset("gone")
