"""Tests for the Google-prefix mapping + system-group routing.

Covers:
- prefix filter (only `grp_acme_*` rows survive into user_group_members)
- login gate (302 when prefix is set and no Workspace group matches)
- system-group mapping (admin/everyone Workspace email → seeded
  Admin/Everyone row instead of a fresh user_groups insert)
- idempotency (second login produces the same memberships)
- API guard `_is_google_managed` + 409 google_managed_readonly
"""

from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import AsyncMock

import pytest
from fastapi.testclient import TestClient


@pytest.fixture
def google_callback_env(tmp_path, monkeypatch):
    """TestClient for the Google callback wired against monkeypatched OAuth.

    Patches `is_available`, `oauth.google.authorize_access_token`, and
    `app.auth.group_sync.fetch_user_groups` so no real network traffic is
    required. The callback's domain check accepts `tester@example.com`
    because no `allowed_domains` is configured by default in tests.

    Per-test setup: monkeypatch the prefix/admin/everyone env vars and the
    `fetch_user_groups` return value before issuing the callback request.
    """
    monkeypatch.setenv("DATA_DIR", str(tmp_path))
    monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-32chars-minimum!!!!!")

    from app.main import create_app
    import app.auth.providers.google as g_mod

    monkeypatch.setattr(g_mod, "is_available", lambda: True)
    fake_oauth_google = SimpleNamespace(
        authorize_access_token=AsyncMock(
            return_value={
                "userinfo": {
                    "email": "tester@example.com",
                    "name": "Tester",
                }
            }
        )
    )
    monkeypatch.setattr(g_mod.oauth, "google", fake_oauth_google, raising=False)

    app = create_app()
    return {
        "client": TestClient(app, follow_redirects=False),
        "monkeypatch": monkeypatch,
        "g_mod": g_mod,
    }


def _set_fetch(monkeypatch, groups):
    import app.auth.group_sync as gs_mod
    monkeypatch.setattr(gs_mod, "fetch_user_groups", lambda email: list(groups))


def _system_db():
    from src.db import get_system_db
    return get_system_db()


class TestPrefixFilter:
    def test_prefix_filter_keeps_only_matching_groups(self, google_callback_env):
        env = google_callback_env
        env["monkeypatch"].setenv("AGNES_GOOGLE_GROUP_PREFIX", "grp_acme_")
        _set_fetch(env["monkeypatch"], [
            "grp_acme_finance@example.com",
            "grp_acme_eng@example.com",
            "grp_other@example.com",
            "acme-everyone@example.com",
            "drinks@example.com",
        ])

        resp = env["client"].get("/auth/google/callback?code=x&state=y")
        assert resp.status_code == 302
        assert resp.headers["location"] == "/dashboard"

        conn = _system_db()
        try:
            from src.repositories.users import UserRepository
            from src.repositories.user_groups import UserGroupsRepository
            from src.repositories.user_group_members import (
                UserGroupMembersRepository,
            )

            user = UserRepository(conn).get_by_email("tester@example.com")
            assert user is not None

            group_ids = UserGroupMembersRepository(conn).list_groups_for_user(
                user["id"]
            )
            ug = UserGroupsRepository(conn)
            names = sorted(ug.get(gid)["name"] for gid in group_ids)
            assert names == [
                "grp_acme_eng@example.com",
                "grp_acme_finance@example.com",
            ]
            for n in names:
                assert ug.get_by_name(n)["created_by"] == "system:google-sync"
        finally:
            conn.close()

    def test_prefix_set_no_match_redirects_to_login_error(
        self, google_callback_env
    ):
        env = google_callback_env
        env["monkeypatch"].setenv("AGNES_GOOGLE_GROUP_PREFIX", "grp_acme_")
        _set_fetch(env["monkeypatch"], [
            "drinks@example.com",
            "acme-everyone@example.com",
        ])

        resp = env["client"].get("/auth/google/callback?code=x&state=y")
        # Bare RedirectResponse defaults to 307 (matches the other error
        # redirects in google.py — domain_not_allowed, oauth_failed, etc.).
        assert resp.status_code in (302, 307)
        assert resp.headers["location"] == "/login?error=not_in_allowed_group"

        # No group memberships were written for the user (the gate fired
        # before replace_google_sync_groups). The user row may exist
        # because user creation happens before the gate — that's the
        # documented behavior; admins can mark the row inactive if they
        # want a hard block.
        conn = _system_db()
        try:
            from src.repositories.users import UserRepository
            from src.repositories.user_group_members import (
                UserGroupMembersRepository,
            )

            user = UserRepository(conn).get_by_email("tester@example.com")
            if user:
                groups = UserGroupMembersRepository(conn).list_groups_for_user(
                    user["id"]
                )
                assert groups == []
        finally:
            conn.close()

    def test_no_prefix_means_legacy_behavior(self, google_callback_env):
        """Without AGNES_GOOGLE_GROUP_PREFIX, every fetched group is mirrored."""
        env = google_callback_env
        env["monkeypatch"].delenv("AGNES_GOOGLE_GROUP_PREFIX", raising=False)
        _set_fetch(env["monkeypatch"], [
            "grp_a@example.com",
            "grp_b@example.com",
        ])

        resp = env["client"].get("/auth/google/callback?code=x&state=y")
        assert resp.status_code == 302
        assert resp.headers["location"] == "/dashboard"

        conn = _system_db()
        try:
            from src.repositories.users import UserRepository
            from src.repositories.user_groups import UserGroupsRepository
            from src.repositories.user_group_members import (
                UserGroupMembersRepository,
            )

            user = UserRepository(conn).get_by_email("tester@example.com")
            group_ids = UserGroupMembersRepository(conn).list_groups_for_user(
                user["id"]
            )
            names = sorted(
                UserGroupsRepository(conn).get(gid)["name"] for gid in group_ids
            )
            assert names == ["grp_a@example.com", "grp_b@example.com"]
        finally:
            conn.close()


class TestSystemMapping:
    def test_admin_email_routes_to_seeded_admin_row(self, google_callback_env):
        env = google_callback_env
        env["monkeypatch"].setenv("AGNES_GOOGLE_GROUP_PREFIX", "grp_acme_")
        env["monkeypatch"].setenv(
            "AGNES_GROUP_ADMIN_EMAIL", "grp_acme_admin@example.com"
        )
        _set_fetch(env["monkeypatch"], [
            "grp_acme_admin@example.com",
            "grp_acme_finance@example.com",
        ])

        env["client"].get("/auth/google/callback?code=x&state=y")

        conn = _system_db()
        try:
            from src.repositories.users import UserRepository
            from src.repositories.user_groups import UserGroupsRepository
            from src.repositories.user_group_members import (
                UserGroupMembersRepository,
            )

            ug = UserGroupsRepository(conn)
            # Crucially: no separate user_groups row was created with the
            # full admin email as `name`. Membership lands in the seeded
            # Admin row instead.
            assert ug.get_by_name("grp_acme_admin@example.com") is None

            admin_row = ug.get_by_name("Admin")
            assert admin_row is not None and admin_row["is_system"] is True

            user = UserRepository(conn).get_by_email("tester@example.com")
            group_ids = UserGroupMembersRepository(conn).list_groups_for_user(
                user["id"]
            )
            assert admin_row["id"] in group_ids

            # Finance group still goes through ensure() and creates a fresh row.
            finance = ug.get_by_name("grp_acme_finance@example.com")
            assert finance is not None
            assert finance["is_system"] is False
            assert finance["created_by"] == "system:google-sync"
            assert finance["id"] in group_ids
        finally:
            conn.close()

    def test_everyone_email_routes_to_seeded_everyone_row(
        self, google_callback_env
    ):
        env = google_callback_env
        env["monkeypatch"].setenv("AGNES_GOOGLE_GROUP_PREFIX", "grp_acme_")
        env["monkeypatch"].setenv(
            "AGNES_GROUP_EVERYONE_EMAIL", "grp_acme_everyone@example.com"
        )
        _set_fetch(env["monkeypatch"], [
            "grp_acme_everyone@example.com",
        ])

        env["client"].get("/auth/google/callback?code=x&state=y")

        conn = _system_db()
        try:
            from src.repositories.users import UserRepository
            from src.repositories.user_groups import UserGroupsRepository
            from src.repositories.user_group_members import (
                UserGroupMembersRepository,
            )

            ug = UserGroupsRepository(conn)
            assert ug.get_by_name("grp_acme_everyone@example.com") is None

            everyone_row = ug.get_by_name("Everyone")
            assert everyone_row is not None
            assert everyone_row["is_system"] is True

            user = UserRepository(conn).get_by_email("tester@example.com")
            group_ids = UserGroupMembersRepository(conn).list_groups_for_user(
                user["id"]
            )
            assert everyone_row["id"] in group_ids
        finally:
            conn.close()


class TestIdempotency:
    def test_second_login_does_not_duplicate_groups(self, google_callback_env):
        env = google_callback_env
        env["monkeypatch"].setenv("AGNES_GOOGLE_GROUP_PREFIX", "grp_acme_")
        _set_fetch(env["monkeypatch"], [
            "grp_acme_finance@example.com",
        ])

        env["client"].get("/auth/google/callback?code=x&state=y")
        env["client"].get("/auth/google/callback?code=x&state=y")

        conn = _system_db()
        try:
            from src.repositories.users import UserRepository
            from src.repositories.user_group_members import (
                UserGroupMembersRepository,
            )

            user = UserRepository(conn).get_by_email("tester@example.com")
            group_ids = UserGroupMembersRepository(conn).list_groups_for_user(
                user["id"]
            )
            # Exactly one membership: same group, deduplicated by the
            # (user_id, group_id) PK in user_group_members.
            assert len(group_ids) == 1

            # Exactly one user_groups row for that name (ensure() is
            # get-or-create, the second login picks up the existing row).
            count = conn.execute(
                "SELECT COUNT(*) FROM user_groups WHERE name = ?",
                ["grp_acme_finance@example.com"],
            ).fetchone()[0]
            assert count == 1
        finally:
            conn.close()


class TestIsGoogleManagedFlag:
    """Exercises the `_is_google_managed` rule used by GroupResponse +
    the API guard."""

    def test_google_sync_row_is_managed(self, tmp_path, monkeypatch):
        monkeypatch.setenv("DATA_DIR", str(tmp_path))
        from app.api.access import _is_google_managed
        g = {
            "name": "grp_acme_x@example.com",
            "is_system": False,
            "created_by": "system:google-sync",
        }
        assert _is_google_managed(g) is True

    def test_system_admin_with_env_mapping_is_managed(self, tmp_path, monkeypatch):
        monkeypatch.setenv("DATA_DIR", str(tmp_path))
        monkeypatch.setenv(
            "AGNES_GROUP_ADMIN_EMAIL", "grp_acme_admin@example.com"
        )
        from app.api.access import _is_google_managed
        g = {"name": "Admin", "is_system": True, "created_by": "system:seed"}
        assert _is_google_managed(g) is True

    def test_system_admin_without_env_mapping_is_not_managed(
        self, tmp_path, monkeypatch
    ):
        monkeypatch.setenv("DATA_DIR", str(tmp_path))
        monkeypatch.delenv("AGNES_GROUP_ADMIN_EMAIL", raising=False)
        monkeypatch.delenv("AGNES_GROUP_EVERYONE_EMAIL", raising=False)
        from app.api.access import _is_google_managed
        g = {"name": "Admin", "is_system": True, "created_by": "system:seed"}
        assert _is_google_managed(g) is False

    def test_manual_custom_group_is_not_managed(self, tmp_path, monkeypatch):
        monkeypatch.setenv("DATA_DIR", str(tmp_path))
        from app.api.access import _is_google_managed
        g = {
            "name": "data-team",
            "is_system": False,
            "created_by": "alice@example.com",
        }
        assert _is_google_managed(g) is False


class TestApiGuard:
    """API endpoints reject mutations on Google-managed groups with 409."""

    @pytest.fixture
    def admin_client(self, tmp_path, monkeypatch):
        monkeypatch.setenv("DATA_DIR", str(tmp_path))
        monkeypatch.setenv("JWT_SECRET_KEY", "test-secret-32chars-minimum!!!!!")
        monkeypatch.setenv(
            "AGNES_GROUP_ADMIN_EMAIL", "grp_acme_admin@example.com"
        )

        from app.main import create_app
        from src.db import get_system_db
        from src.repositories.users import UserRepository
        from src.repositories.user_groups import UserGroupsRepository
        from src.repositories.user_group_members import (
            UserGroupMembersRepository,
        )
        from app.auth.jwt import create_access_token

        conn = get_system_db()
        try:
            ur = UserRepository(conn)
            ur.create(id="admin1", email="admin@x", name="Admin1")
            ur.create(id="u1", email="u1@x", name="U1")
            ug = UserGroupsRepository(conn)
            admin_id = ug.get_by_name("Admin")["id"]
            UserGroupMembersRepository(conn).add_member(
                "admin1", admin_id, source="system_seed",
            )
            # A google-sync group to act on.
            ug.ensure("grp_acme_finance@example.com")
        finally:
            conn.close()

        app = create_app()
        client = TestClient(app, follow_redirects=False)
        token = create_access_token("admin1", "admin@x")
        client.cookies.set("access_token", token)
        return client

    def _gid(self, name):
        from src.db import get_system_db
        from src.repositories.user_groups import UserGroupsRepository
        conn = get_system_db()
        try:
            return UserGroupsRepository(conn).get_by_name(name)["id"]
        finally:
            conn.close()

    def test_patch_google_managed_returns_409(self, admin_client):
        gid = self._gid("grp_acme_finance@example.com")
        r = admin_client.patch(
            f"/api/admin/groups/{gid}",
            json={"name": "renamed"},
        )
        assert r.status_code == 409
        body = r.json()
        # FastAPI wraps the dict detail under "detail"; assert the code is
        # surfaced for the UI's machine-readable branch.
        assert body["detail"]["code"] == "google_managed_readonly"

    def test_delete_google_managed_returns_409(self, admin_client):
        gid = self._gid("grp_acme_finance@example.com")
        r = admin_client.delete(f"/api/admin/groups/{gid}")
        assert r.status_code == 409
        assert r.json()["detail"]["code"] == "google_managed_readonly"

    def test_add_member_to_google_managed_returns_409(self, admin_client):
        gid = self._gid("grp_acme_finance@example.com")
        r = admin_client.post(
            f"/api/admin/groups/{gid}/members",
            json={"email": "u1@x"},
        )
        assert r.status_code == 409
        assert r.json()["detail"]["code"] == "google_managed_readonly"

    def test_patch_admin_with_env_mapping_returns_409(self, admin_client):
        # AGNES_GROUP_ADMIN_EMAIL is set in the fixture → seeded Admin row
        # is treated as Google-managed and rejects renames here too.
        gid = self._gid("Admin")
        r = admin_client.patch(
            f"/api/admin/groups/{gid}",
            json={"description": "updated"},
        )
        assert r.status_code == 409
        assert r.json()["detail"]["code"] == "google_managed_readonly"
