"""
Tests for multi-tenant owner_id scoping — critical audit fixes (PR #1593).

Covers: TOCTOU-safe delete_summary, cascade rollback on ownership failure,
batch message ownership JOINs, and cross-tenant DAG edge safety.
"""

from __future__ import annotations

import pytest
import pytest_asyncio

from cortex.hippo.config import HippoConfig
from cortex.hippo.engine import HippoEngine


OWNER_A = "owner-alice"
OWNER_B = "owner-bob"


@pytest.fixture
def mt_config() -> HippoConfig:
    return HippoConfig(enabled=True, db_path=":memory:", fresh_tail_count=4, max_tokens=10_000)


@pytest_asyncio.fixture
async def engine(mt_config: HippoConfig) -> HippoEngine:
    eng = HippoEngine(config=mt_config)
    await eng.initialize(db_path=":memory:", owner_id=OWNER_A, conversation_id="sess-a")
    yield eng
    await eng.shutdown()


async def _mk_summary(store, conv_id, owner_id, content="test", token_count=50):
    return await store.create_summary(
        conversation_id=conv_id, owner_id=owner_id, kind="leaf",
        depth=0, content=content, token_count=token_count,
    )


# ── C-1: delete_summary TOCTOU + scoped cascade ──────────────────────


@pytest.mark.asyncio
async def test_delete_summary_does_not_corrupt_other_tenant_dag_edges(engine: HippoEngine):
    """Deleting owner A's summary must not remove owner B's own DAG edges.

    Cross-tenant orphaned edges (pointing to the deleted summary) ARE cleaned
    up due to FK constraints, but Bob's edges to his own summaries survive.
    """
    store = engine.summary_store
    conv_id = engine._conversation.conversation_id

    sum_a = await _mk_summary(store, conv_id, OWNER_A, content="Alice summary")
    sum_b = await _mk_summary(store, conv_id, OWNER_B, content="Bob summary")
    sum_b_child = await _mk_summary(store, conv_id, OWNER_B, content="Bob child")

    # Bob's child has two parents: Bob's summary + Alice's (cross-tenant)
    await store.add_summary_parents(
        sum_b_child.summary_id, [sum_b.summary_id, sum_a.summary_id]
    )

    conn = store._conn
    async with conn.execute(
        "SELECT COUNT(*) FROM hippo_summary_parents WHERE summary_id = ?",
        (sum_b_child.summary_id,),
    ) as cur:
        assert (await cur.fetchone())[0] == 2

    # Alice deletes her summary
    result = await store.delete_summary(sum_a.summary_id, owner_id=OWNER_A)
    assert result is True

    # Alice's summary is gone
    assert await store.get_summary(OWNER_A, sum_a.summary_id) is None

    # Bob's summaries survive
    assert await store.get_summary(OWNER_B, sum_b.summary_id) is not None
    assert await store.get_summary(OWNER_B, sum_b_child.summary_id) is not None

    # Bob's own parent edge survives
    async with conn.execute(
        "SELECT parent_summary_id FROM hippo_summary_parents WHERE summary_id = ?",
        (sum_b_child.summary_id,),
    ) as cur:
        rows = await cur.fetchall()
        parent_ids = {r[0] for r in rows}

    assert sum_b.summary_id in parent_ids, "Bob's own parent edge must survive"
    assert sum_a.summary_id not in parent_ids, "Orphaned cross-tenant edge cleaned up"


@pytest.mark.asyncio
async def test_delete_summary_wrong_owner_rejected(engine: HippoEngine):
    """Owner A cannot delete owner B's summary."""
    store = engine.summary_store
    conv_id = engine._conversation.conversation_id

    sum_b = await _mk_summary(store, conv_id, OWNER_B, content="Bob's data")
    result = await store.delete_summary(sum_b.summary_id, owner_id=OWNER_A)
    assert result is False
    assert await store.get_summary(OWNER_B, sum_b.summary_id) is not None


@pytest.mark.asyncio
async def test_delete_summary_wrong_owner_rollback_preserves_edges(engine: HippoEngine):
    """Failed ownership check must rollback cascade deletes — edges stay."""
    store = engine.summary_store
    conv_id = engine._conversation.conversation_id

    sum_b = await _mk_summary(store, conv_id, OWNER_B, content="Bob parent")
    sum_b_child = await _mk_summary(store, conv_id, OWNER_B, content="Bob child")
    await store.add_summary_parents(sum_b_child.summary_id, [sum_b.summary_id])

    # Alice tries to delete Bob's parent — should fail and rollback
    result = await store.delete_summary(sum_b.summary_id, owner_id=OWNER_A)
    assert result is False

    # Summary survives
    assert await store.get_summary(OWNER_B, sum_b.summary_id) is not None

    # DAG edges survive the rollback
    conn = store._conn
    async with conn.execute(
        "SELECT COUNT(*) FROM hippo_summary_parents WHERE parent_summary_id = ?",
        (sum_b.summary_id,),
    ) as cur:
        assert (await cur.fetchone())[0] == 1, "Parent edge must survive rollback"


@pytest.mark.asyncio
async def test_delete_summary_without_owner_id_backward_compat(engine: HippoEngine):
    """Delete without owner_id still works (backward compat)."""
    store = engine.summary_store
    conv_id = engine._conversation.conversation_id

    summary = await _mk_summary(store, conv_id, OWNER_A, content="test")
    result = await store.delete_summary(summary.summary_id)
    assert result is True
    assert await store.get_summary(OWNER_A, summary.summary_id) is None


@pytest.mark.asyncio
async def test_delete_nonexistent_summary_returns_false(engine: HippoEngine):
    """Deleting a non-existent summary returns False."""
    store = engine.summary_store
    result = await store.delete_summary("sum_nonexistent", owner_id=OWNER_A)
    assert result is False


# ── H-1: get_source_message_ids_batch message ownership JOIN ──────────


@pytest.mark.asyncio
async def test_get_source_message_ids_batch_owner_scoped(engine: HippoEngine):
    """Batch message ID fetch verifies both summary AND message ownership."""
    store = engine.summary_store
    conv_store = engine.conversation_store
    conv_id = engine._conversation.conversation_id

    sum_a = await _mk_summary(store, conv_id, OWNER_A, content="Alice leaf")
    sum_b = await _mk_summary(store, conv_id, OWNER_B, content="Bob leaf")

    msg_a = await conv_store.create_message(
        conversation_id=conv_id, owner_id=OWNER_A, role="user", content="Alice msg",
    )
    msg_b = await conv_store.create_message(
        conversation_id=conv_id, owner_id=OWNER_B, role="user", content="Bob msg",
    )

    await store.link_summary_messages(sum_a.summary_id, [msg_a.message_id])
    await store.link_summary_messages(sum_b.summary_id, [msg_b.message_id])

    # Alice should only see her own messages
    result_a = await store.get_source_message_ids_batch(
        [sum_a.summary_id, sum_b.summary_id], owner_id=OWNER_A
    )
    assert len(result_a[sum_a.summary_id]) == 1
    assert len(result_a[sum_b.summary_id]) == 0, "Alice must NOT see Bob's messages"

    # Without owner_id, both are visible
    result_all = await store.get_source_message_ids_batch(
        [sum_a.summary_id, sum_b.summary_id]
    )
    assert len(result_all[sum_a.summary_id]) == 1
    assert len(result_all[sum_b.summary_id]) == 1
