"""Unit tests for cookie, category, and allow-list routers — mocked database."""

import uuid
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock

import pytest
from httpx import ASGITransport, AsyncClient

from src.main import create_app
from src.services.auth import create_access_token

ORG_ID = uuid.uuid4()
USER_ID = uuid.uuid4()


def _auth_headers(role="owner"):
    token = create_access_token(
        user_id=USER_ID, organisation_id=ORG_ID, role=role, email="admin@test.com"
    )
    return {"Authorization": f"Bearer {token}"}


def _mock_category(**overrides):
    cat = MagicMock(spec=[])
    cat.id = overrides.get("id", uuid.uuid4())
    cat.name = overrides.get("name", "Analytics")
    cat.slug = overrides.get("slug", "analytics")
    cat.description = overrides.get("description", "Analytics cookies")
    cat.is_essential = overrides.get("is_essential", False)
    cat.display_order = overrides.get("display_order", 3)
    cat.tcf_purpose_ids = overrides.get("tcf_purpose_ids", [])
    cat.gcm_consent_types = overrides.get("gcm_consent_types", ["analytics_storage"])
    cat.created_at = datetime.now(UTC)
    cat.updated_at = datetime.now(UTC)
    return cat


def _mock_cookie(**overrides):
    cookie = MagicMock(spec=[])
    cookie.id = overrides.get("id", uuid.uuid4())
    cookie.site_id = overrides.get("site_id", uuid.uuid4())
    cookie.name = overrides.get("name", "_ga")
    cookie.domain = overrides.get("domain", ".google.com")
    cookie.path = overrides.get("path", "/")
    cookie.category_id = overrides.get("category_id")
    cookie.storage_type = overrides.get("storage_type", "cookie")
    cookie.review_status = overrides.get("review_status", "pending")
    cookie.description = overrides.get("description")
    cookie.vendor = overrides.get("vendor")
    cookie.max_age_seconds = overrides.get("max_age_seconds")
    cookie.is_http_only = overrides.get("is_http_only")
    cookie.is_secure = overrides.get("is_secure")
    cookie.same_site = overrides.get("same_site")
    cookie.first_seen_at = overrides.get("first_seen_at", datetime.now(UTC).isoformat())
    cookie.last_seen_at = overrides.get("last_seen_at", datetime.now(UTC).isoformat())
    cookie.created_at = datetime.now(UTC)
    cookie.updated_at = datetime.now(UTC)
    return cookie


def _mock_site():
    site = MagicMock(spec=[])
    site.id = uuid.uuid4()
    site.organisation_id = ORG_ID
    site.domain = "test.com"
    site.deleted_at = None
    return site


def _mock_allow_list_entry(**overrides):
    entry = MagicMock(spec=[])
    entry.id = overrides.get("id", uuid.uuid4())
    entry.site_id = overrides.get("site_id", uuid.uuid4())
    entry.name_pattern = overrides.get("name_pattern", "_ga*")
    entry.domain_pattern = overrides.get("domain_pattern", ".google.com")
    entry.category_id = overrides.get("category_id", uuid.uuid4())
    entry.description = overrides.get("description")
    entry.created_at = datetime.now(UTC)
    entry.updated_at = datetime.now(UTC)
    return entry


def _mock_db_sequence(*results):
    session = AsyncMock()
    mock_results = []
    for r in results:
        result = MagicMock()
        if isinstance(r, list):
            result.scalar_one_or_none.return_value = r[0] if r else None
            scalars_obj = MagicMock()
            scalars_obj.all.return_value = r
            result.scalars.return_value = scalars_obj
        elif isinstance(r, dict) and "scalar" in r:
            result.scalar.return_value = r["scalar"]
        elif isinstance(r, dict) and "all" in r:
            result.all.return_value = r["all"]
        else:
            result.scalar_one_or_none.return_value = r
        mock_results.append(result)
    session.execute = AsyncMock(side_effect=mock_results)

    _added = []

    def _fake_add(obj):
        _added.append(obj)

    session.add = MagicMock(side_effect=_fake_add)

    async def _fake_flush():
        for obj in _added:
            if getattr(obj, "id", None) is None:
                obj.id = uuid.uuid4()
            if hasattr(obj, "review_status") and getattr(obj, "review_status", None) is None:
                obj.review_status = "pending"
            if hasattr(obj, "created_at") and getattr(obj, "created_at", None) is None:
                obj.created_at = datetime.now(UTC)
            if hasattr(obj, "updated_at") and getattr(obj, "updated_at", None) is None:
                obj.updated_at = datetime.now(UTC)

    session.flush = AsyncMock(side_effect=_fake_flush)
    session.refresh = AsyncMock()
    session.delete = AsyncMock()
    return session


@pytest.fixture
def mock_app():
    return create_app()


async def _client(app, mock_session):
    from src.db import get_db

    async def _override():
        yield mock_session

    app.dependency_overrides[get_db] = _override
    transport = ASGITransport(app=app)
    return AsyncClient(transport=transport, base_url="http://test")


class TestCookieCategories:
    @pytest.mark.asyncio
    async def test_list_categories(self, mock_app):
        cats = [_mock_category(slug="necessary"), _mock_category(slug="analytics")]
        db = _mock_db_sequence(cats)
        async with await _client(mock_app, db) as client:
            resp = await client.get("/api/v1/cookies/categories")
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_get_category(self, mock_app):
        cat = _mock_category()
        db = _mock_db_sequence(cat)
        async with await _client(mock_app, db) as client:
            resp = await client.get(f"/api/v1/cookies/categories/{cat.id}")
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_get_category_not_found(self, mock_app):
        db = _mock_db_sequence(None)
        async with await _client(mock_app, db) as client:
            resp = await client.get(f"/api/v1/cookies/categories/{uuid.uuid4()}")
        assert resp.status_code == 404


class TestCookieCRUD:
    @pytest.mark.asyncio
    async def test_list_cookies(self, mock_app):
        site = _mock_site()
        cookies = [_mock_cookie(site_id=site.id)]
        db = _mock_db_sequence(site, cookies)
        async with await _client(mock_app, db) as client:
            resp = await client.get(f"/api/v1/cookies/sites/{site.id}", headers=_auth_headers())
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_list_cookies_empty(self, mock_app):
        site = _mock_site()
        db = _mock_db_sequence(site, [])
        async with await _client(mock_app, db) as client:
            resp = await client.get(f"/api/v1/cookies/sites/{site.id}", headers=_auth_headers())
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_create_cookie(self, mock_app):
        site = _mock_site()
        db = _mock_db_sequence(site)  # site found
        async with await _client(mock_app, db) as client:
            resp = await client.post(
                f"/api/v1/cookies/sites/{site.id}",
                json={"name": "_ga", "domain": ".google.com"},
                headers=_auth_headers(),
            )
        assert resp.status_code == 201

    @pytest.mark.asyncio
    async def test_create_cookie_with_invalid_category(self, mock_app):
        site = _mock_site()
        cat_id = uuid.uuid4()
        db = _mock_db_sequence(site, None)  # site found, category not found
        async with await _client(mock_app, db) as client:
            resp = await client.post(
                f"/api/v1/cookies/sites/{site.id}",
                json={"name": "_ga", "domain": ".google.com", "category_id": str(cat_id)},
                headers=_auth_headers(),
            )
        assert resp.status_code == 400

    @pytest.mark.asyncio
    async def test_get_cookie(self, mock_app):
        site = _mock_site()
        cookie = _mock_cookie(site_id=site.id)
        db = _mock_db_sequence(site, cookie)
        async with await _client(mock_app, db) as client:
            resp = await client.get(
                f"/api/v1/cookies/sites/{site.id}/{cookie.id}",
                headers=_auth_headers(),
            )
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_get_cookie_not_found(self, mock_app):
        site = _mock_site()
        db = _mock_db_sequence(site, None)
        async with await _client(mock_app, db) as client:
            resp = await client.get(
                f"/api/v1/cookies/sites/{site.id}/{uuid.uuid4()}",
                headers=_auth_headers(),
            )
        assert resp.status_code == 404

    @pytest.mark.asyncio
    async def test_update_cookie(self, mock_app):
        site = _mock_site()
        cookie = _mock_cookie(site_id=site.id)
        db = _mock_db_sequence(site, cookie)
        async with await _client(mock_app, db) as client:
            resp = await client.patch(
                f"/api/v1/cookies/sites/{site.id}/{cookie.id}",
                json={"review_status": "approved"},
                headers=_auth_headers(),
            )
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_update_cookie_not_found(self, mock_app):
        site = _mock_site()
        db = _mock_db_sequence(site, None)
        async with await _client(mock_app, db) as client:
            resp = await client.patch(
                f"/api/v1/cookies/sites/{site.id}/{uuid.uuid4()}",
                json={"review_status": "approved"},
                headers=_auth_headers(),
            )
        assert resp.status_code == 404

    @pytest.mark.asyncio
    async def test_update_cookie_invalid_category(self, mock_app):
        site = _mock_site()
        cookie = _mock_cookie(site_id=site.id)
        cat_id = uuid.uuid4()
        # site found, cookie found, category validation fails
        db = _mock_db_sequence(site, cookie, None)
        async with await _client(mock_app, db) as client:
            resp = await client.patch(
                f"/api/v1/cookies/sites/{site.id}/{cookie.id}",
                json={"category_id": str(cat_id)},
                headers=_auth_headers(),
            )
        assert resp.status_code == 400

    @pytest.mark.asyncio
    async def test_delete_cookie(self, mock_app):
        site = _mock_site()
        cookie = _mock_cookie(site_id=site.id)
        db = _mock_db_sequence(site, cookie)
        async with await _client(mock_app, db) as client:
            resp = await client.delete(
                f"/api/v1/cookies/sites/{site.id}/{cookie.id}",
                headers=_auth_headers(),
            )
        assert resp.status_code == 204

    @pytest.mark.asyncio
    async def test_delete_cookie_not_found(self, mock_app):
        site = _mock_site()
        db = _mock_db_sequence(site, None)
        async with await _client(mock_app, db) as client:
            resp = await client.delete(
                f"/api/v1/cookies/sites/{site.id}/{uuid.uuid4()}",
                headers=_auth_headers(),
            )
        assert resp.status_code == 404


class TestCookieSummary:
    @pytest.mark.asyncio
    async def test_cookie_summary(self, mock_app):
        site = _mock_site()
        # summary makes 4 queries: _get_org_site, status count, category count, uncategorised
        db = _mock_db_sequence(
            site,
            {"all": [("pending", 5), ("approved", 3)]},
            {"all": [("analytics", 4), ("marketing", 2)]},
            {"scalar": 2},
        )
        async with await _client(mock_app, db) as client:
            resp = await client.get(
                f"/api/v1/cookies/sites/{site.id}/summary",
                headers=_auth_headers(),
            )
        assert resp.status_code == 200
        data = resp.json()
        assert "total" in data
        assert "by_status" in data
        assert "uncategorised" in data


class TestAllowList:
    @pytest.mark.asyncio
    @pytest.mark.asyncio
    async def test_list_allow_list(self, mock_app):
        site = _mock_site()
        entries = [_mock_allow_list_entry(site_id=site.id)]
        db = _mock_db_sequence(site, entries)
        async with await _client(mock_app, db) as client:
            resp = await client.get(
                f"/api/v1/cookies/sites/{site.id}/allow-list",
                headers=_auth_headers(),
            )
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_create_allow_list_entry(self, mock_app):
        site = _mock_site()
        cat = _mock_category()
        db = _mock_db_sequence(site, cat)  # site found, category valid
        async with await _client(mock_app, db) as client:
            resp = await client.post(
                f"/api/v1/cookies/sites/{site.id}/allow-list",
                json={
                    "name_pattern": "_ga*",
                    "domain_pattern": ".google.com",
                    "category_id": str(cat.id),
                },
                headers=_auth_headers(),
            )
        assert resp.status_code == 201

    @pytest.mark.asyncio
    async def test_create_allow_list_invalid_category(self, mock_app):
        site = _mock_site()
        db = _mock_db_sequence(site, None)  # site found, category not found
        async with await _client(mock_app, db) as client:
            resp = await client.post(
                f"/api/v1/cookies/sites/{site.id}/allow-list",
                json={
                    "name_pattern": "_ga*",
                    "domain_pattern": ".google.com",
                    "category_id": str(uuid.uuid4()),
                },
                headers=_auth_headers(),
            )
        assert resp.status_code == 400

    @pytest.mark.asyncio
    async def test_update_allow_list_entry(self, mock_app):
        site = _mock_site()
        entry = _mock_allow_list_entry(site_id=site.id)
        db = _mock_db_sequence(site, entry)
        async with await _client(mock_app, db) as client:
            resp = await client.patch(
                f"/api/v1/cookies/sites/{site.id}/allow-list/{entry.id}",
                json={"name_pattern": "_fbp*"},
                headers=_auth_headers(),
            )
        assert resp.status_code == 200

    @pytest.mark.asyncio
    async def test_update_allow_list_not_found(self, mock_app):
        site = _mock_site()
        db = _mock_db_sequence(site, None)
        async with await _client(mock_app, db) as client:
            resp = await client.patch(
                f"/api/v1/cookies/sites/{site.id}/allow-list/{uuid.uuid4()}",
                json={"name_pattern": "_fbp*"},
                headers=_auth_headers(),
            )
        assert resp.status_code == 404

    @pytest.mark.asyncio
    async def test_update_allow_list_invalid_category(self, mock_app):
        site = _mock_site()
        entry = _mock_allow_list_entry(site_id=site.id)
        db = _mock_db_sequence(site, entry, None)  # site, entry found, category invalid
        async with await _client(mock_app, db) as client:
            resp = await client.patch(
                f"/api/v1/cookies/sites/{site.id}/allow-list/{entry.id}",
                json={"category_id": str(uuid.uuid4())},
                headers=_auth_headers(),
            )
        assert resp.status_code == 400

    @pytest.mark.asyncio
    async def test_delete_allow_list_entry(self, mock_app):
        site = _mock_site()
        entry = _mock_allow_list_entry(site_id=site.id)
        db = _mock_db_sequence(site, entry)
        async with await _client(mock_app, db) as client:
            resp = await client.delete(
                f"/api/v1/cookies/sites/{site.id}/allow-list/{entry.id}",
                headers=_auth_headers(),
            )
        assert resp.status_code == 204

    @pytest.mark.asyncio
    async def test_delete_allow_list_not_found(self, mock_app):
        site = _mock_site()
        db = _mock_db_sequence(site, None)
        async with await _client(mock_app, db) as client:
            resp = await client.delete(
                f"/api/v1/cookies/sites/{site.id}/allow-list/{uuid.uuid4()}",
                headers=_auth_headers(),
            )
        assert resp.status_code == 404

    @pytest.mark.asyncio
    async def test_site_not_found(self, mock_app):
        db = _mock_db_sequence(None)  # site not found
        async with await _client(mock_app, db) as client:
            resp = await client.get(
                f"/api/v1/cookies/sites/{uuid.uuid4()}",
                headers=_auth_headers(),
            )
        assert resp.status_code == 404
