from __future__ import annotations

from typing import Any

import pytest

from agent.dashboard import user_mappings as um


class _FakeStore:
    """Minimal in-memory stand-in for the LangGraph Store."""

    def __init__(self) -> None:
        self.items: dict[tuple[tuple[str, ...], str], dict[str, Any]] = {}

    async def get_item(self, namespace: list[str], key: str):
        value = self.items.get((tuple(namespace), key))
        return {"value": value} if value is not None else None

    async def put_item(self, namespace: list[str], key: str, value: dict[str, Any]) -> None:
        self.items[(tuple(namespace), key)] = value

    async def delete_item(self, namespace: list[str], key: str) -> None:
        self.items.pop((tuple(namespace), key), None)

    async def search_items(self, namespace: list[str], *, limit: int = 1000):
        ns = tuple(namespace)
        items = [{"value": v} for (n, _k), v in self.items.items() if n == ns]
        return {"items": items[:limit]}


class _FakeClient:
    def __init__(self, store: _FakeStore) -> None:
        self.store = store


@pytest.fixture()
def fake_store(monkeypatch: pytest.MonkeyPatch) -> _FakeStore:
    store = _FakeStore()
    monkeypatch.setattr(um, "_client", lambda: _FakeClient(store))
    um.clear_cache()
    return store


@pytest.mark.asyncio
async def test_upsert_and_bidirectional_lookup(fake_store: _FakeStore) -> None:
    await um.upsert_mapping(
        github_login="Octocat",
        work_email="OCTO@example.com",
        slack_user_id="U123",
    )
    # Login lookups are case-insensitive; email is normalized to lowercase.
    assert await um.email_for_login("octocat") == "octo@example.com"
    assert await um.login_for_email("octo@example.com") == "Octocat"
    assert await um.login_for_slack_id("U123") == "Octocat"


@pytest.mark.asyncio
async def test_cache_readers_after_refresh(fake_store: _FakeStore) -> None:
    await um.upsert_mapping(github_login="dev", work_email="dev@x.com")
    um.clear_cache()
    await um.refresh_cache()
    assert um.cached_email_for_login("dev") == "dev@x.com"
    assert um.cached_login_for_email("dev@x.com") == "dev"
    assert um.is_login_mapped("dev") is True
    assert um.is_login_mapped("ghost") is False


@pytest.mark.asyncio
async def test_pending_status_not_trusted(fake_store: _FakeStore) -> None:
    await um.upsert_mapping(github_login="newbie", work_email="n@x.com", status="pending")
    um.clear_cache()
    await um.refresh_cache()
    assert um.is_login_mapped("newbie") is False


@pytest.mark.asyncio
async def test_delete_removes_record_and_indexes(fake_store: _FakeStore) -> None:
    await um.upsert_mapping(github_login="gone", work_email="g@x.com")
    assert await um.email_for_login("gone") == "g@x.com"
    deleted = await um.delete_mapping("gone")
    assert deleted is True
    assert um.cached_email_for_login("gone") is None
    assert await um.get_mapping("gone") is None


@pytest.mark.asyncio
async def test_resolve_login_from_email_async_cold_cache(
    fake_store: _FakeStore, monkeypatch: pytest.MonkeyPatch
) -> None:
    # Mapped user must resolve even on a cold worker (cache not yet primed),
    # because repo-resolution call sites run before the cache is refreshed.
    from agent.dashboard import agent_overrides

    monkeypatch.setattr(agent_overrides, "login_for_email", um.login_for_email)
    await um.upsert_mapping(github_login="cold", work_email="cold@x.com")
    um.clear_cache()

    assert await agent_overrides.resolve_login_from_email_async("cold@x.com") == "cold"


@pytest.mark.asyncio
async def test_update_deindexes_stale_email_and_slack_id(fake_store: _FakeStore) -> None:
    # An update that changes the email/slack id must not leave the old aliases
    # resolving to this login in the in-process cache.
    await um.upsert_mapping(
        github_login="mover",
        work_email="old@x.com",
        slack_user_id="UOLD",
    )
    await um.upsert_mapping(
        github_login="mover",
        work_email="new@x.com",
        slack_user_id="UNEW",
    )

    assert um.cached_login_for_email("old@x.com") is None
    assert um.cached_login_for_slack_id("UOLD") is None
    assert um.cached_login_for_email("new@x.com") == "mover"
    assert um.cached_login_for_slack_id("UNEW") == "mover"


@pytest.mark.asyncio
async def test_upsert_requires_login_and_email(fake_store: _FakeStore) -> None:
    with pytest.raises(ValueError):
        await um.upsert_mapping(github_login="", work_email="x@x.com")
    with pytest.raises(ValueError):
        await um.upsert_mapping(github_login="x", work_email="")


@pytest.mark.asyncio
async def test_list_mappings_sorted(fake_store: _FakeStore) -> None:
    await um.upsert_mapping(github_login="zeta", work_email="z@x.com")
    await um.upsert_mapping(github_login="alpha", work_email="a@x.com")
    listed = await um.list_mappings()
    assert [m["github_login"] for m in listed] == ["alpha", "zeta"]
