"""Integration tests for Subject Snapshots feature.

Tests cover:
- Feature flag gating (disabled → 404)
- Snapshot creation via service layer
- Restore by ID and by name
- Provenance remapping
- Timestamp shifting
- Cleanup safety (snapshot subjects not cleaned)
- System subject hiding from list_subjects
"""

from __future__ import annotations

import uuid
from unittest.mock import patch

import pytest
from httpx import AsyncClient


# ---------------------------------------------------------------------------
# Feature flag: disabled → 404
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_snapshots_disabled_returns_404(client: AsyncClient):
    """When enable_snapshots=False, admin snapshot endpoints return 404."""
    with patch("server.core.config.settings.enable_snapshots", False):
        r = await client.get("/admin/snapshots")
        assert r.status_code == 404

        r = await client.post(
            "/admin/snapshots/restore-by-name",
            json={"name": "test", "target_subject_id": "x"},
        )
        assert r.status_code == 404

        r = await client.post("/admin/cleanup")
        assert r.status_code == 404


# ---------------------------------------------------------------------------
# Full lifecycle: create snapshot via service, restore via API
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_snapshot_create_and_restore(client: AsyncClient, subject_id: str):
    """Create a snapshot from episodes, then restore into a new subject."""
    with patch("server.core.config.settings.enable_snapshots", True):
        # 1. Ingest episodes into source subject
        episodes = [
            {
                "subject_id": subject_id,
                "source": "test",
                "type": "support-chat",
                "payload": {
                    "messages": [
                        {"role": "user", "content": "My name is Alice"},
                        {"role": "assistant", "content": "Hi Alice!"},
                    ]
                },
            },
            {
                "subject_id": subject_id,
                "source": "test",
                "type": "support-chat",
                "payload": {
                    "messages": [
                        {"role": "user", "content": "I'm on the pro plan"},
                        {"role": "assistant", "content": "Great, pro plan noted."},
                    ]
                },
            },
        ]
        for ep in episodes:
            r = await client.post("/v1/episodes", json=ep)
            assert r.status_code == 201

        # 2. Compile
        r = await client.post("/v1/memories/compile", json={"subject_id": subject_id})
        assert r.status_code == 200

        # 3. Create snapshot via service (create endpoint removed from API)
        from server.services.snapshots import create_snapshot

        snap_name = f"test-snap-{uuid.uuid4().hex[:8]}"
        snap = await create_snapshot(name=snap_name, source_subject_id=subject_id)
        assert snap["name"] == snap_name
        assert snap["episode_count"] >= 2
        assert snap["memory_count"] >= 1

        # 4. Verify snapshot appears in list
        r = await client.get("/admin/snapshots")
        assert r.status_code == 200
        names = [s["name"] for s in r.json()["snapshots"]]
        assert snap_name in names

        # 5. Restore by ID into target subject
        target_id = f"restored-{uuid.uuid4().hex[:8]}"
        r = await client.post(
            f"/admin/snapshots/{snap['id']}/restore",
            json={"target_subject_id": target_id},
        )
        assert r.status_code == 200
        result = r.json()
        assert result["episodes_restored"] >= 2
        assert result["memories_restored"] >= 1

        # 6. Target subject should have searchable memories
        r = await client.get(
            "/v1/memories/search",
            params={"subject_id": target_id, "limit": 50},
        )
        assert r.status_code == 200
        mems = r.json()["memories"]
        assert len(mems) >= 1


@pytest.mark.anyio
async def test_restored_subject_behaves_normally(client: AsyncClient, subject_id: str):
    """A restored subject should work like any organic subject — ingest, compile, search, context."""
    with patch("server.core.config.settings.enable_snapshots", True):
        # Setup and restore
        await client.post(
            "/v1/episodes",
            json={
                "subject_id": subject_id,
                "source": "test",
                "type": "support-chat",
                "payload": {
                    "messages": [
                        {"role": "user", "content": "My name is Bob"},
                        {"role": "assistant", "content": "Hi Bob!"},
                    ]
                },
            },
        )
        await client.post("/v1/memories/compile", json={"subject_id": subject_id})

        from server.services.snapshots import create_snapshot

        snap_name = f"normal-{uuid.uuid4().hex[:8]}"
        snap = await create_snapshot(name=snap_name, source_subject_id=subject_id)

        target_id = f"normal-target-{uuid.uuid4().hex[:8]}"
        await client.post(
            f"/admin/snapshots/{snap['id']}/restore",
            json={"target_subject_id": target_id},
        )

        # Now use target_id like any normal subject
        # 1. Ingest new episode
        r = await client.post(
            "/v1/episodes",
            json={
                "subject_id": target_id,
                "source": "test",
                "type": "support-chat",
                "payload": {
                    "messages": [
                        {"role": "user", "content": "I upgraded to enterprise"},
                        {"role": "assistant", "content": "Great!"},
                    ]
                },
            },
        )
        assert r.status_code == 201

        # 2. Re-compile (should not fail)
        r = await client.post("/v1/memories/compile", json={"subject_id": target_id})
        assert r.status_code == 200

        # 3. Context retrieval works
        r = await client.post(
            "/v1/context",
            json={
                "subject_id": target_id,
                "task": "Help Bob with billing",
            },
        )
        assert r.status_code == 200
        assert r.json()["subject_id"] == target_id

        # 4. Timeline works
        r = await client.get("/v1/timeline", params={"subject_id": target_id})
        assert r.status_code == 200

        # 5. Shows up in subject list
        r = await client.get("/v1/subjects")
        assert r.status_code == 200
        subject_ids = [s["subject_id"] for s in r.json().get("subjects", r.json().get("items", []))]
        assert target_id in subject_ids


@pytest.mark.anyio
async def test_timestamp_shifting_lands_near_now(client: AsyncClient, subject_id: str):
    """Restored episodes should have timestamps near now, preserving relative order."""
    import datetime

    with patch("server.core.config.settings.enable_snapshots", True):
        # Ingest two episodes
        r1 = await client.post(
            "/v1/episodes",
            json={
                "subject_id": subject_id,
                "source": "test",
                "type": "note",
                "payload": {"text": "first event"},
            },
        )
        assert r1.status_code == 201

        # Small delay to ensure ordering
        import asyncio

        await asyncio.sleep(0.1)

        r2 = await client.post(
            "/v1/episodes",
            json={
                "subject_id": subject_id,
                "source": "test",
                "type": "note",
                "payload": {"text": "second event"},
            },
        )
        assert r2.status_code == 201

        await client.post("/v1/memories/compile", json={"subject_id": subject_id})

        from server.services.snapshots import create_snapshot

        snap_name = f"time-{uuid.uuid4().hex[:8]}"
        snap = await create_snapshot(name=snap_name, source_subject_id=subject_id)

        target_id = f"time-target-{uuid.uuid4().hex[:8]}"
        restore_resp = await client.post(
            f"/admin/snapshots/{snap['id']}/restore",
            json={"target_subject_id": target_id},
        )
        assert restore_resp.status_code == 200, f"Restore failed: {restore_resp.text}"
        restore_result = restore_resp.json()
        assert restore_result["episodes_restored"] >= 2, f"Expected at least 2 episodes restored: {restore_result}"

        # Fetch timeline for restored subject
        r = await client.get("/v1/timeline", params={"subject_id": target_id})
        assert r.status_code == 200
        timeline = r.json()
        episodes = timeline.get("episodes", [])
        assert len(episodes) >= 2, f"Expected at least 2 episodes, got {len(episodes)}: {timeline}"

        # All timestamps should be within the last 60 seconds
        now = datetime.datetime.now(datetime.timezone.utc)
        for event in episodes:
            ts = datetime.datetime.fromisoformat(event["created_at"].replace("Z", "+00:00"))
            delta = (now - ts).total_seconds()
            assert delta < 60, f"Restored timestamp too old: {delta}s ago"

        # Relative order preserved (second event after first)
        timestamps = [
            datetime.datetime.fromisoformat(e["created_at"].replace("Z", "+00:00")) for e in episodes
        ]
        assert timestamps == sorted(timestamps)


@pytest.mark.anyio
async def test_snapshot_restore_by_name(client: AsyncClient, subject_id: str):
    """Restore a snapshot using the by-name endpoint."""
    with patch("server.core.config.settings.enable_snapshots", True):
        # Setup: ingest + compile + create snapshot
        r = await client.post(
            "/v1/episodes",
            json={
                "subject_id": subject_id,
                "source": "test",
                "type": "note",
                "payload": {"text": "User prefers dark mode"},
            },
        )
        assert r.status_code == 201
        await client.post("/v1/memories/compile", json={"subject_id": subject_id})

        from server.services.snapshots import create_snapshot

        snap_name = f"byname-{uuid.uuid4().hex[:8]}"
        await create_snapshot(name=snap_name, source_subject_id=subject_id)

        # Restore by name
        target_id = f"live_test_{uuid.uuid4().hex[:8]}"
        r = await client.post(
            "/admin/snapshots/restore-by-name",
            json={"name": snap_name, "target_subject_id": target_id},
        )
        assert r.status_code == 200
        assert r.json()["episodes_restored"] >= 1


# ---------------------------------------------------------------------------
# Provenance and timestamps
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_restored_provenance_uses_new_ids(client: AsyncClient, subject_id: str):
    """Restored memories should reference the new episode IDs, not originals."""
    with patch("server.core.config.settings.enable_snapshots", True):
        r = await client.post(
            "/v1/episodes",
            json={
                "subject_id": subject_id,
                "source": "test",
                "type": "support-chat",
                "payload": {
                    "messages": [
                        {"role": "user", "content": "I use vim"},
                        {"role": "assistant", "content": "Noted!"},
                    ]
                },
            },
        )
        assert r.status_code == 201
        original_ep_id = r.json()["id"]

        await client.post("/v1/memories/compile", json={"subject_id": subject_id})

        from server.services.snapshots import create_snapshot

        snap_name = f"prov-{uuid.uuid4().hex[:8]}"
        snap = await create_snapshot(name=snap_name, source_subject_id=subject_id)

        target_id = f"prov-target-{uuid.uuid4().hex[:8]}"
        r = await client.post(
            f"/admin/snapshots/{snap['id']}/restore",
            json={"target_subject_id": target_id},
        )
        assert r.status_code == 200

        # Check memories — source_episode_ids should NOT contain original
        r = await client.get(
            "/v1/memories/search",
            params={"subject_id": target_id, "limit": 50},
        )
        mems = r.json()["memories"]
        for mem in mems:
            if mem.get("source_episode_ids"):
                assert original_ep_id not in mem["source_episode_ids"]


# ---------------------------------------------------------------------------
# System subject hiding
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_snapshot_subjects_hidden_from_timeline(client: AsyncClient, subject_id: str):
    """Snapshot source subjects (_snapshot/*) should not appear in subject listing."""
    with patch("server.core.config.settings.enable_snapshots", True):
        # Create an episode so the subject appears
        await client.post(
            "/v1/episodes",
            json={
                "subject_id": subject_id,
                "source": "test",
                "type": "note",
                "payload": {"text": "hello"},
            },
        )
        await client.post("/v1/memories/compile", json={"subject_id": subject_id})

        from server.services.snapshots import create_snapshot

        snap_name = f"hidden-{uuid.uuid4().hex[:8]}"
        await create_snapshot(name=snap_name, source_subject_id=subject_id)

        # List subjects (via timeline which uses list_subjects)
        r = await client.get("/v1/subjects")
        if r.status_code == 200:
            subjects = [
                s["subject_id"] for s in r.json().get("subjects", r.json().get("items", []))
            ]
            for s in subjects:
                assert not s.startswith("_snapshot/")
                assert not s.startswith("_bootstrap_tmp/")


# ---------------------------------------------------------------------------
# Cleanup safety
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_cleanup_does_not_delete_snapshots(client: AsyncClient, subject_id: str):
    """Cleanup of stale live_ subjects must never delete _snapshot/* subjects."""
    with patch("server.core.config.settings.enable_snapshots", True):
        # Create a snapshot
        await client.post(
            "/v1/episodes",
            json={
                "subject_id": subject_id,
                "source": "test",
                "type": "note",
                "payload": {"text": "preserve me"},
            },
        )
        await client.post("/v1/memories/compile", json={"subject_id": subject_id})

        from server.services.snapshots import create_snapshot

        snap_name = f"cleanup-{uuid.uuid4().hex[:8]}"
        snap = await create_snapshot(name=snap_name, source_subject_id=subject_id)

        # Run cleanup with 0 hours TTL (would catch everything)
        r = await client.post("/admin/cleanup", params={"prefix": "live_", "max_age_hours": 0})
        assert r.status_code == 200

        # Snapshot should still exist
        r = await client.get("/admin/snapshots")
        snap_ids = [s["id"] for s in r.json()["snapshots"]]
        assert snap["id"] in snap_ids


# ---------------------------------------------------------------------------
# Snapshot data-integrity: occurred_at preservation + resolution cleanup
# ---------------------------------------------------------------------------


@pytest.mark.anyio
async def test_create_snapshot_preserves_occurred_at(client: AsyncClient, subject_id, session_factory):
    """The snapshot-source episode must carry the original episode's
    occurred_at, not the column server-default (now()). Otherwise a
    backfilled history (occurred_at far in the past) collapses its real
    timeline to snapshot-creation time on every snapshot round-trip."""
    import datetime as dt

    from sqlalchemy import select

    from server.db.tables import EpisodeRow
    from server.services.snapshots import SNAPSHOT_SOURCE_PREFIX, create_snapshot

    expected = dt.datetime(2020, 1, 2, 3, 4, 5, tzinfo=dt.timezone.utc)
    with patch("server.core.config.settings.enable_snapshots", True):
        r = await client.post(
            "/v1/episodes",
            json={
                "subject_id": subject_id,
                "source": "test",
                "type": "conversation",
                "payload": {"text": "backfilled history"},
                "occurred_at": expected.isoformat(),
            },
        )
        assert r.status_code == 201

        snap_name = f"occ-{uuid.uuid4().hex[:8]}"
        await create_snapshot(name=snap_name, source_subject_id=subject_id)

    snapshot_subject = f"{SNAPSHOT_SOURCE_PREFIX}{snap_name}/v1"
    async with session_factory() as session:
        rows = (
            await session.execute(
                select(EpisodeRow).where(EpisodeRow.subject_id == snapshot_subject)
            )
        ).scalars().all()
    assert rows, "snapshot-source episode should exist"
    assert all(row.occurred_at == expected for row in rows)


@pytest.mark.anyio
async def test_delete_snapshot_removes_copied_resolutions(
    client: AsyncClient, subject_id, session_factory
):
    """delete_snapshot must reap the resolutions create_snapshot copied into
    the snapshot-source subject, otherwise they orphan under the deleted
    _snapshot/* subject and grow the table unbounded."""
    from sqlalchemy import func, select

    from server.db.tables import EpisodeRow, ResolutionRow
    from server.services.snapshots import (
        SNAPSHOT_SOURCE_PREFIX,
        create_snapshot,
        delete_snapshot,
    )

    async with session_factory() as session:
        session.add(
            EpisodeRow(
                id=uuid.uuid4(),
                subject_id=subject_id,
                source="test",
                type="conversation",
                payload={"text": "hi"},
            )
        )
        session.add(
            ResolutionRow(
                id=uuid.uuid4(),
                subject_id=subject_id,
                session_id="sess-1",
                status="resolved",
                resolution_summary="done",
            )
        )
        await session.commit()

    snapshot_subject = ""
    with patch("server.core.config.settings.enable_snapshots", True):
        snap_name = f"res-{uuid.uuid4().hex[:8]}"
        snap = await create_snapshot(name=snap_name, source_subject_id=subject_id)
        snapshot_subject = f"{SNAPSHOT_SOURCE_PREFIX}{snap_name}/v1"

        async with session_factory() as session:
            copied = await session.scalar(
                select(func.count())
                .select_from(ResolutionRow)
                .where(ResolutionRow.subject_id == snapshot_subject)
            )
        assert copied == 1, "create_snapshot should copy the resolution into the source"

        assert await delete_snapshot(uuid.UUID(snap["id"])) is True

        listing = await client.get("/admin/snapshots")
        assert snap["id"] not in [s["id"] for s in listing.json()["snapshots"]]

    async with session_factory() as session:
        remaining = await session.scalar(
            select(func.count())
            .select_from(ResolutionRow)
            .where(ResolutionRow.subject_id == snapshot_subject)
        )
    assert remaining == 0
