"""Tests for the campaign-clustering read API.

Mirrors :mod:`tests.web.test_api_identities` for the layer above.
The campaign clusterer is a separate worker; these tests cover the
read-only API which ships in the same wave. Empty-table behaviour,
soft-merge resolution, and pagination forwarding are the headline
cases.
"""
from datetime import datetime, timezone
from unittest.mock import AsyncMock, patch

import pytest
from fastapi import HTTPException


def _campaign_row(
    uuid: str = "c-uuid-1",
    merged_into_uuid: str | None = None,
    identity_count: int = 0,
) -> dict:
    now = datetime(2026, 4, 26, tzinfo=timezone.utc).isoformat()
    return {
        "uuid": uuid,
        "schema_version": 1,
        "first_seen_at": None,
        "last_seen_at": None,
        "created_at": now,
        "updated_at": now,
        "confidence": None,
        "identity_count": identity_count,
        "ja3_hashes": None,
        "hassh_hashes": None,
        "payload_simhashes": None,
        "c2_endpoints": None,
        "merged_into_uuid": merged_into_uuid,
        "notes": None,
    }


def _identity_row(uuid: str, campaign_id: str | None) -> dict:
    return {
        "uuid": uuid,
        "schema_version": 1,
        "campaign_id": campaign_id,
        "merged_into_uuid": None,
    }


# ─── GET /campaigns ──────────────────────────────────────────────────────────


class TestListCampaigns:
    @pytest.mark.asyncio
    async def test_empty_table_returns_zero_total(self):
        from decnet.web.router.campaigns.api_list_campaigns import list_campaigns

        with patch(
            "decnet.web.router.campaigns.api_list_campaigns.repo"
        ) as mock_repo:
            mock_repo.list_campaigns = AsyncMock(return_value=[])
            mock_repo.count_campaigns = AsyncMock(return_value=0)

            result = await list_campaigns(
                limit=50, offset=0, user={"uuid": "u", "role": "viewer"}
            )

        assert result == {"total": 0, "limit": 50, "offset": 0, "data": []}

    @pytest.mark.asyncio
    async def test_returns_seeded_data(self):
        from decnet.web.router.campaigns.api_list_campaigns import list_campaigns

        rows = [_campaign_row(f"c-{n}") for n in range(3)]
        with patch(
            "decnet.web.router.campaigns.api_list_campaigns.repo"
        ) as mock_repo:
            mock_repo.list_campaigns = AsyncMock(return_value=rows)
            mock_repo.count_campaigns = AsyncMock(return_value=3)

            result = await list_campaigns(
                limit=50, offset=0, user={"uuid": "u", "role": "viewer"}
            )

        assert result["total"] == 3
        assert [r["uuid"] for r in result["data"]] == ["c-0", "c-1", "c-2"]

    @pytest.mark.asyncio
    async def test_pagination_args_forwarded(self):
        from decnet.web.router.campaigns.api_list_campaigns import list_campaigns

        with patch(
            "decnet.web.router.campaigns.api_list_campaigns.repo"
        ) as mock_repo:
            mock_repo.list_campaigns = AsyncMock(return_value=[])
            mock_repo.count_campaigns = AsyncMock(return_value=0)

            await list_campaigns(
                limit=10, offset=20, user={"uuid": "u", "role": "viewer"}
            )

        mock_repo.list_campaigns.assert_awaited_once_with(limit=10, offset=20)


# ─── GET /campaigns/{uuid} ───────────────────────────────────────────────────


class TestGetCampaignDetail:
    @pytest.mark.asyncio
    async def test_404_on_missing_uuid(self):
        from decnet.web.router.campaigns.api_get_campaign_detail import (
            get_campaign_detail,
        )

        with patch(
            "decnet.web.router.campaigns.api_get_campaign_detail.repo"
        ) as mock_repo:
            mock_repo.get_campaign_by_uuid = AsyncMock(return_value=None)

            with pytest.raises(HTTPException) as exc:
                await get_campaign_detail(
                    uuid="ghost", user={"uuid": "u", "role": "viewer"}
                )
            assert exc.value.status_code == 404

    @pytest.mark.asyncio
    async def test_returns_campaign_with_live_identity_count(self):
        from decnet.web.router.campaigns.api_get_campaign_detail import (
            get_campaign_detail,
        )

        campaign = _campaign_row("c-real", identity_count=2)
        with patch(
            "decnet.web.router.campaigns.api_get_campaign_detail.repo"
        ) as mock_repo:
            mock_repo.get_campaign_by_uuid = AsyncMock(return_value=campaign)
            mock_repo.count_identities_for_campaign = AsyncMock(return_value=5)

            result = await get_campaign_detail(
                uuid="c-real", user={"uuid": "u", "role": "viewer"}
            )

        assert result["uuid"] == "c-real"
        assert result["identity_count_live"] == 5
        assert result["identity_count"] == 2


# ─── GET /campaigns/{uuid}/identities ────────────────────────────────────────


class TestListCampaignIdentities:
    @pytest.mark.asyncio
    async def test_404_when_campaign_missing(self):
        from decnet.web.router.campaigns.api_list_campaign_identities import (
            list_campaign_identities,
        )

        with patch(
            "decnet.web.router.campaigns.api_list_campaign_identities.repo"
        ) as mock_repo:
            mock_repo.get_campaign_by_uuid = AsyncMock(return_value=None)

            with pytest.raises(HTTPException) as exc:
                await list_campaign_identities(
                    uuid="ghost", limit=50, offset=0,
                    user={"uuid": "u", "role": "viewer"},
                )
            assert exc.value.status_code == 404

    @pytest.mark.asyncio
    async def test_returns_identities_for_existing_campaign(self):
        from decnet.web.router.campaigns.api_list_campaign_identities import (
            list_campaign_identities,
        )

        campaign = _campaign_row("c-real")
        idents = [
            _identity_row("i-1", "c-real"),
            _identity_row("i-2", "c-real"),
        ]
        with patch(
            "decnet.web.router.campaigns.api_list_campaign_identities.repo"
        ) as mock_repo:
            mock_repo.get_campaign_by_uuid = AsyncMock(return_value=campaign)
            mock_repo.list_identities_for_campaign = AsyncMock(return_value=idents)
            mock_repo.count_identities_for_campaign = AsyncMock(return_value=2)

            result = await list_campaign_identities(
                uuid="c-real", limit=50, offset=0,
                user={"uuid": "u", "role": "viewer"},
            )

        assert result["total"] == 2
        assert [r["uuid"] for r in result["data"]] == ["i-1", "i-2"]

    @pytest.mark.asyncio
    async def test_merged_uuid_resolves_to_winners_identities(self):
        """Soft-merged campaigns: identities are listed under the winner."""
        from decnet.web.router.campaigns.api_list_campaign_identities import (
            list_campaign_identities,
        )

        winner = _campaign_row("c-winner")
        with patch(
            "decnet.web.router.campaigns.api_list_campaign_identities.repo"
        ) as mock_repo:
            mock_repo.get_campaign_by_uuid = AsyncMock(return_value=winner)
            mock_repo.list_identities_for_campaign = AsyncMock(return_value=[])
            mock_repo.count_identities_for_campaign = AsyncMock(return_value=0)

            await list_campaign_identities(
                uuid="c-loser", limit=50, offset=0,
                user={"uuid": "u", "role": "viewer"},
            )

        mock_repo.list_identities_for_campaign.assert_awaited_once_with(
            "c-winner", limit=50, offset=0,
        )


# ─── Repo-level integration ──────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_repo_methods_against_empty_schema(tmp_path):
    from decnet.web.db.factory import get_repository

    repo = get_repository(db_path=str(tmp_path / "campaigns.db"))
    await repo.initialize()

    assert await repo.list_campaigns(limit=50, offset=0) == []
    assert await repo.count_campaigns() == 0
    assert await repo.get_campaign_by_uuid("anything") is None
    assert await repo.list_identities_for_campaign("anything") == []
    assert await repo.count_identities_for_campaign("anything") == 0


@pytest.mark.asyncio
async def test_repo_follows_campaign_merge_chain(tmp_path):
    from decnet.web.db.factory import get_repository

    repo = get_repository(db_path=str(tmp_path / "merge.db"))
    await repo.initialize()
    await repo.create_campaign({"uuid": "winner-uuid"})
    await repo.create_campaign(
        {"uuid": "loser-uuid", "merged_into_uuid": "winner-uuid"}
    )

    resolved = await repo.get_campaign_by_uuid("loser-uuid")
    assert resolved is not None
    assert resolved["uuid"] == "winner-uuid"

    direct = await repo.get_campaign_by_uuid("winner-uuid")
    assert direct["uuid"] == "winner-uuid"
    assert direct["merged_into_uuid"] is None
