"""tests/test_entity_lifecycle.py — Entity visibility lifecycle + connection_count.

Tests for migration v20: entity lifecycle columns, trigger, view, backfill.
Issue: #933
"""
from __future__ import annotations

import aiosqlite
import pytest
import pytest_asyncio

from cortex.storage.migrations.v3_initial import SQL_UP as V3_SQL
from cortex.storage.migrations.v7_peers import SQL_UP as V7_SQL


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

async def _bootstrap_db() -> aiosqlite.Connection:
    """Create an in-memory DB with entity + semantic_claim + peer tables at v19."""
    conn = await aiosqlite.connect(":memory:")
    conn.row_factory = aiosqlite.Row

    # Apply v3 initial schema (entity, semantic_claim, etc.)
    await conn.executescript(V3_SQL)

    # Apply v7 peer table
    await conn.executescript(V7_SQL)

    # Set schema_version to 19
    await conn.execute(
        "INSERT OR REPLACE INTO cortex_config (key, value) VALUES ('schema_version', '19')"
    )
    await conn.commit()
    return conn


async def _apply_v20(conn: aiosqlite.Connection) -> None:
    """Apply the v20 migration SQL."""
    from cortex.storage.migrations.v21_entity_lifecycle import SQL_UP
    await conn.executescript(SQL_UP)
    await conn.execute(
        "UPDATE cortex_config SET value = '20' WHERE key = 'schema_version'"
    )
    await conn.commit()


async def _insert_entity(
    conn: aiosqlite.Connection,
    entity_id: str,
    owner_id: str = "test",
    name: str = "TestEntity",
    entity_type: str = "concept",
    visibility_status: str = "extracted",
) -> str:
    """Insert an entity and return its ID."""
    await conn.execute(
        """INSERT INTO entity (id, owner_id, canonical_name, entity_type, visibility_status)
           VALUES (?, ?, ?, ?, ?)""",
        (entity_id, owner_id, name, entity_type, visibility_status),
    )
    await conn.commit()
    return entity_id


async def _insert_claim(
    conn: aiosqlite.Connection,
    claim_id: str,
    owner_id: str = "test",
    subject_entity_id: str | None = None,
    object_entity_id: str | None = None,
    status: str = "active",
) -> str:
    """Insert a semantic claim and return its ID."""
    await conn.execute(
        """INSERT INTO semantic_claim
           (id, owner_id, subject, predicate, object_value,
            memory_type, status, subject_entity_id, object_entity_id)
           VALUES (?, ?, 'subj', 'pred', 'obj', 'semantic', ?, ?, ?)""",
        (claim_id, owner_id, status, subject_entity_id, object_entity_id),
    )
    await conn.commit()
    return claim_id


async def _get_entity_connection_count(conn: aiosqlite.Connection, entity_id: str) -> int:
    async with conn.execute(
        "SELECT connection_count FROM entity WHERE id = ?", (entity_id,)
    ) as cur:
        row = await cur.fetchone()
    return row["connection_count"] if row else -1


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------

class TestMigrationApplies:
    @pytest.mark.asyncio
    async def test_migration_applies_cleanly(self):
        conn = await _bootstrap_db()
        await _apply_v20(conn)

        # Verify schema_version
        async with conn.execute(
            "SELECT value FROM cortex_config WHERE key = 'schema_version'"
        ) as cur:
            row = await cur.fetchone()
        assert row["value"] == "20"
        await conn.close()

    @pytest.mark.asyncio
    async def test_new_columns_have_correct_defaults(self):
        conn = await _bootstrap_db()
        await _apply_v20(conn)

        # Insert entity with defaults
        await conn.execute(
            "INSERT INTO entity (id, owner_id, canonical_name, entity_type) "
            "VALUES ('e1', 'test', 'Test', 'concept')"
        )
        await conn.commit()

        async with conn.execute("SELECT * FROM entity WHERE id = 'e1'") as cur:
            row = await cur.fetchone()

        assert row["visibility_status"] == "extracted"
        assert row["connection_count"] == 0
        assert row["last_promoted_at"] is None
        assert row["promotion_reason"] is None
        await conn.close()


class TestTriggerConnectionCount:
    @pytest.mark.asyncio
    async def test_claim_insert_updates_subject_entity(self):
        conn = await _bootstrap_db()
        await _apply_v20(conn)

        await _insert_entity(conn, "e1")
        assert await _get_entity_connection_count(conn, "e1") == 0

        # Insert a claim with subject_entity_id = e1
        await _insert_claim(conn, "c1", subject_entity_id="e1")
        assert await _get_entity_connection_count(conn, "e1") == 1

        # Insert another claim
        await _insert_claim(conn, "c2", subject_entity_id="e1")
        assert await _get_entity_connection_count(conn, "e1") == 2
        await conn.close()

    @pytest.mark.asyncio
    async def test_claim_insert_updates_both_entities(self):
        conn = await _bootstrap_db()
        await _apply_v20(conn)

        await _insert_entity(conn, "e1", name="Entity1")
        await _insert_entity(conn, "e2", name="Entity2")

        # Insert claim linking both
        await _insert_claim(conn, "c1", subject_entity_id="e1", object_entity_id="e2")

        # Both should have connection_count = 1
        assert await _get_entity_connection_count(conn, "e1") == 1
        assert await _get_entity_connection_count(conn, "e2") == 1
        await conn.close()

    @pytest.mark.asyncio
    async def test_inactive_claims_not_counted(self):
        conn = await _bootstrap_db()
        await _apply_v20(conn)

        await _insert_entity(conn, "e1")

        # Insert an inactive claim — trigger still fires but count query filters by status='active'
        await _insert_claim(conn, "c1", subject_entity_id="e1", status="superseded")
        assert await _get_entity_connection_count(conn, "e1") == 0
        await conn.close()

    @pytest.mark.asyncio
    async def test_entity_without_claims_has_zero(self):
        conn = await _bootstrap_db()
        await _apply_v20(conn)

        await _insert_entity(conn, "e1")
        assert await _get_entity_connection_count(conn, "e1") == 0
        await conn.close()


class TestBackfill:
    @pytest.mark.asyncio
    async def test_backfill_counts_existing_claims(self):
        conn = await _bootstrap_db()

        # Insert entity and claims BEFORE migration
        await conn.execute(
            "INSERT INTO entity (id, owner_id, canonical_name, entity_type) "
            "VALUES ('e1', 'test', 'PreExisting', 'concept')"
        )
        await conn.execute(
            """INSERT INTO semantic_claim
               (id, owner_id, subject, predicate, object_value,
                memory_type, status, subject_entity_id)
               VALUES ('c1', 'test', 's', 'p', 'o', 'semantic', 'active', 'e1')"""
        )
        await conn.execute(
            """INSERT INTO semantic_claim
               (id, owner_id, subject, predicate, object_value,
                memory_type, status, object_entity_id)
               VALUES ('c2', 'test', 's', 'p', 'o', 'semantic', 'active', 'e1')"""
        )
        await conn.commit()

        # Now apply migration — backfill should set connection_count
        await _apply_v20(conn)

        count = await _get_entity_connection_count(conn, "e1")
        assert count == 2
        await conn.close()


class TestDashboardView:
    @pytest.mark.asyncio
    async def test_view_returns_promoted_and_active_only(self):
        conn = await _bootstrap_db()
        await _apply_v20(conn)

        # Create entities with different visibility statuses
        await _insert_entity(conn, "e1", name="Promoted", visibility_status="promoted")
        await _insert_entity(conn, "e2", name="Active", visibility_status="active")
        await _insert_entity(conn, "e3", name="Extracted", visibility_status="extracted")

        async with conn.execute("SELECT * FROM dashboard_entities") as cur:
            rows = await cur.fetchall()

        names = [row["name"] for row in rows]
        assert "Promoted" in names
        assert "Active" in names
        assert "Extracted" not in names
        await conn.close()

    @pytest.mark.asyncio
    async def test_view_includes_peers_with_known_relationship(self):
        conn = await _bootstrap_db()
        await _apply_v20(conn)

        # Insert a peer with known relationship
        await conn.execute(
            """INSERT INTO peer
               (id, owner_id, display_name, relationship,
                first_interaction, last_interaction, interaction_count,
                created_at, updated_at)
               VALUES ('p1', 'test', 'Andrew', 'friend',
                       datetime('now'), datetime('now'), 5,
                       datetime('now'), datetime('now'))"""
        )
        # Insert a peer with unknown relationship — should be excluded
        await conn.execute(
            """INSERT INTO peer
               (id, owner_id, display_name, relationship,
                first_interaction, last_interaction, interaction_count,
                created_at, updated_at)
               VALUES ('p2', 'test', 'Stranger', 'unknown',
                       datetime('now'), datetime('now'), 1,
                       datetime('now'), datetime('now'))"""
        )
        await conn.commit()

        async with conn.execute("SELECT * FROM dashboard_entities") as cur:
            rows = await cur.fetchall()

        names = [row["name"] for row in rows]
        assert "Andrew" in names
        assert "Stranger" not in names
        await conn.close()


class TestVisibilityFilter:
    @pytest.mark.asyncio
    async def test_filter_by_visibility_status(self):
        conn = await _bootstrap_db()
        await _apply_v20(conn)

        await _insert_entity(conn, "e1", name="Promoted", visibility_status="promoted")
        await _insert_entity(conn, "e2", name="Extracted", visibility_status="extracted")

        # Query with visibility filter
        async with conn.execute(
            "SELECT * FROM entity WHERE owner_id = 'test' AND visibility_status = 'promoted'"
        ) as cur:
            rows = await cur.fetchall()

        assert len(rows) == 1
        assert rows[0]["canonical_name"] == "Promoted"
        await conn.close()


class TestEntityTypes:
    @pytest.mark.asyncio
    async def test_entity_dataclass_has_new_fields(self):
        from cortex.types import Entity

        e = Entity(
            id="e1",
            owner_id="test",
            canonical_name="Test",
            entity_type="concept",
        )
        assert e.visibility_status == "extracted"
        assert e.connection_count == 0
        assert e.last_promoted_at is None
        assert e.promotion_reason is None

    @pytest.mark.asyncio
    async def test_entity_dataclass_accepts_custom_values(self):
        from cortex.types import Entity

        e = Entity(
            id="e1",
            owner_id="test",
            canonical_name="Test",
            entity_type="concept",
            visibility_status="promoted",
            connection_count=5,
            last_promoted_at="2026-03-16T00:00:00",
            promotion_reason="high_connection_count",
        )
        assert e.visibility_status == "promoted"
        assert e.connection_count == 5
        assert e.last_promoted_at == "2026-03-16T00:00:00"
        assert e.promotion_reason == "high_connection_count"
