"""Tests for orphan session migration audit fixes (B-OS1, B-OS2, B-OS3).

B-OS1: SQL_DOWN preserves original status (lossless rollback)
B-OS2: ISO-date GLOB requires 'T' separator (no false positives)
B-OS3: Claimless sessions marked orphan after skip threshold
"""
from __future__ import annotations

import json
import pytest
import aiosqlite

from cortex.storage.migrations.v26_orphan_sessions import (
    _MARK_ORPHANS_SQL,
    upgrade,
    downgrade,
)


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

async def _create_test_db():
    """Create an in-memory DB with minimal session_record + semantic_claim tables."""
    conn = await aiosqlite.connect(":memory:")
    await conn.execute("""
        CREATE TABLE session_record (
            id TEXT PRIMARY KEY,
            owner_id TEXT NOT NULL DEFAULT 'default',
            session_id TEXT NOT NULL UNIQUE,
            started_at TEXT NOT NULL DEFAULT (datetime('now')),
            ended_at TEXT,
            status TEXT DEFAULT 'awake',
            sleep_report TEXT,
            created_at TEXT NOT NULL DEFAULT (datetime('now'))
        )
    """)
    await conn.execute("""
        CREATE TABLE semantic_claim (
            id TEXT PRIMARY KEY,
            source_session TEXT,
            subject TEXT,
            predicate TEXT,
            object_value TEXT
        )
    """)
    await conn.commit()
    return conn


async def _insert_session(conn, session_id, status="completed", sleep_report=None):
    await conn.execute(
        "INSERT INTO session_record (id, session_id, status, sleep_report) VALUES (?, ?, ?, ?)",
        (session_id, session_id, status, sleep_report),
    )
    await conn.commit()


async def _insert_claim(conn, claim_id, source_session):
    await conn.execute(
        "INSERT INTO semantic_claim (id, source_session) VALUES (?, ?)",
        (claim_id, source_session),
    )
    await conn.commit()


async def _get_session(conn, session_id):
    async with conn.execute(
        "SELECT status, sleep_report FROM session_record WHERE session_id = ?",
        (session_id,),
    ) as cur:
        return await cur.fetchone()


# ===========================================================================
# B-OS1: Lossless rollback — previous_status preserved
# ===========================================================================

class TestBOS1LosslessRollback:
    """SQL_DOWN should restore original status, not blindly set 'completed'."""

    @pytest.mark.asyncio
    async def test_upgrade_stores_previous_status(self):
        """Upgrade should save original status in sleep_report JSON."""
        conn = await _create_test_db()
        try:
            await _insert_session(conn, "batch-sleeping-1", status="sleeping")
            await _insert_session(conn, "batch-dreaming-1", status="dreaming")
            await _insert_session(conn, "batch-completed-1", status="completed")

            await upgrade(conn)

            for sid in ["batch-sleeping-1", "batch-dreaming-1", "batch-completed-1"]:
                row = await _get_session(conn, sid)
                assert row[0] == "orphan", f"{sid} should be orphan"
                report = json.loads(row[1])
                assert "previous_status" in report, f"{sid} missing previous_status"

            # Check specific previous statuses
            row = await _get_session(conn, "batch-sleeping-1")
            assert json.loads(row[1])["previous_status"] == "sleeping"

            row = await _get_session(conn, "batch-dreaming-1")
            assert json.loads(row[1])["previous_status"] == "dreaming"

            row = await _get_session(conn, "batch-completed-1")
            assert json.loads(row[1])["previous_status"] == "completed"
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_downgrade_restores_original_status(self):
        """Downgrade should restore each session to its original status."""
        conn = await _create_test_db()
        try:
            await _insert_session(conn, "batch-sleeping-1", status="sleeping")
            await _insert_session(conn, "batch-dreaming-1", status="dreaming")
            await _insert_session(conn, "batch-completed-1", status="completed")

            await upgrade(conn)

            # Verify all are orphan
            for sid in ["batch-sleeping-1", "batch-dreaming-1", "batch-completed-1"]:
                row = await _get_session(conn, sid)
                assert row[0] == "orphan"

            # Downgrade
            await downgrade(conn)

            # Each should be restored to its original status
            row = await _get_session(conn, "batch-sleeping-1")
            assert row[0] == "sleeping", f"Expected 'sleeping', got '{row[0]}'"

            row = await _get_session(conn, "batch-dreaming-1")
            assert row[0] == "dreaming", f"Expected 'dreaming', got '{row[0]}'"

            row = await _get_session(conn, "batch-completed-1")
            assert row[0] == "completed", f"Expected 'completed', got '{row[0]}'"
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_downgrade_skips_sessions_without_previous_status(self):
        """Sessions manually marked orphan (no previous_status) should be left alone."""
        conn = await _create_test_db()
        try:
            # Manually insert an orphan without previous_status in sleep_report
            await _insert_session(conn, "manual-orphan", status="orphan", sleep_report=None)

            await downgrade(conn)

            row = await _get_session(conn, "manual-orphan")
            assert row[0] == "orphan", "Should remain orphan (no previous_status to restore)"
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_upgrade_skips_awake_sessions(self):
        """Sessions with status='awake' should NOT be marked as orphan."""
        conn = await _create_test_db()
        try:
            await _insert_session(conn, "batch-awake-1", status="awake")

            await upgrade(conn)

            row = await _get_session(conn, "batch-awake-1")
            assert row[0] == "awake", "Awake sessions should not be orphaned"
        finally:
            await conn.close()


# ===========================================================================
# B-OS2: ISO-date GLOB tightened — requires 'T' separator
# ===========================================================================

class TestBOS2GlobPattern:
    """ISO-date GLOB should not match non-batch sessions with date-like IDs."""

    @pytest.mark.asyncio
    async def test_iso_date_only_not_matched(self):
        """A session_id like '2026-03-12-user-session' should NOT be orphaned."""
        conn = await _create_test_db()
        try:
            # This is a legitimate user session with a date-prefixed ID but no 'T'
            await _insert_session(conn, "2026-03-12-user-session", status="completed")

            await upgrade(conn)

            row = await _get_session(conn, "2026-03-12-user-session")
            assert row[0] == "completed", (
                "Date-prefixed user session should NOT be matched by tightened GLOB"
            )
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_iso_timestamp_with_T_matched(self):
        """A batch-ingest session_id like '2026-03-12T14:30:00' SHOULD be orphaned."""
        conn = await _create_test_db()
        try:
            await _insert_session(conn, "2026-03-12T14:30:00-chunk-1", status="completed")

            await upgrade(conn)

            row = await _get_session(conn, "2026-03-12T14:30:00-chunk-1")
            assert row[0] == "orphan", "Full ISO timestamp session should be orphaned"
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_batch_prefix_still_matched(self):
        """Sessions with 'batch-' or 'batch_' prefix should still be matched."""
        conn = await _create_test_db()
        try:
            await _insert_session(conn, "batch-abc-123", status="completed")
            await _insert_session(conn, "batch_xyz_456", status="sleeping")

            await upgrade(conn)

            row = await _get_session(conn, "batch-abc-123")
            assert row[0] == "orphan"

            row = await _get_session(conn, "batch_xyz_456")
            assert row[0] == "orphan"
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_session_with_claims_not_orphaned(self):
        """Even batch-prefixed sessions should NOT be orphaned if they have claims."""
        conn = await _create_test_db()
        try:
            await _insert_session(conn, "batch-has-claims", status="completed")
            await _insert_claim(conn, "claim-1", "batch-has-claims")

            await upgrade(conn)

            row = await _get_session(conn, "batch-has-claims")
            assert row[0] == "completed", "Session with claims should NOT be orphaned"
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_bare_date_id_not_matched(self):
        """A bare date like '2026-03-12' should NOT match the tightened GLOB."""
        conn = await _create_test_db()
        try:
            await _insert_session(conn, "2026-03-12", status="completed")

            await upgrade(conn)

            row = await _get_session(conn, "2026-03-12")
            assert row[0] == "completed", "Bare date ID should NOT be matched"
        finally:
            await conn.close()


# ===========================================================================
# B-OS3: Claimless skip loop — mark orphan after threshold
# ===========================================================================

class TestBOS3ClaimlessSkipLoop:
    """Claimless-awake sessions should be marked orphan after repeated skips."""

    @pytest.mark.asyncio
    async def test_increment_claimless_skip_count(self):
        """Skip count should increment in sleep_report JSON."""
        conn = await _create_test_db()
        try:
            await _insert_session(conn, "sess-claimless-1", status="awake")

            # Simulate the _increment_claimless_skip_count logic
            for expected in range(1, 4):
                async with conn.execute(
                    "SELECT sleep_report FROM session_record WHERE session_id = ?",
                    ("sess-claimless-1",),
                ) as cur:
                    row = await cur.fetchone()

                report_data = {}
                if row and row[0]:
                    report_data = json.loads(row[0])
                count = report_data.get("claimless_skip_count", 0) + 1
                report_data["claimless_skip_count"] = count

                await conn.execute(
                    "UPDATE session_record SET sleep_report = ? WHERE session_id = ?",
                    (json.dumps(report_data), "sess-claimless-1"),
                )
                await conn.commit()

                assert count == expected

            # After 3 increments, count should be 3
            row = await _get_session(conn, "sess-claimless-1")
            report = json.loads(row[1])
            assert report["claimless_skip_count"] == 3
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_mark_session_orphan_preserves_previous_status(self):
        """_mark_session_orphan should store previous_status in sleep_report."""
        conn = await _create_test_db()
        try:
            await _insert_session(conn, "sess-awake-1", status="awake")

            # Simulate _mark_session_orphan
            async with conn.execute(
                "SELECT status, sleep_report FROM session_record WHERE session_id = ?",
                ("sess-awake-1",),
            ) as cur:
                row = await cur.fetchone()

            report_data = {}
            if row[1]:
                report_data = json.loads(row[1])
            report_data["previous_status"] = row[0]

            await conn.execute(
                "UPDATE session_record SET status = 'orphan', sleep_report = ? WHERE session_id = ?",
                (json.dumps(report_data), "sess-awake-1"),
            )
            await conn.commit()

            row = await _get_session(conn, "sess-awake-1")
            assert row[0] == "orphan"
            report = json.loads(row[1])
            assert report["previous_status"] == "awake"
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_skip_count_preserved_when_marking_orphan(self):
        """When marking orphan, existing skip_count should be preserved in sleep_report."""
        conn = await _create_test_db()
        try:
            # Session with existing skip count
            report = json.dumps({"claimless_skip_count": 3})
            await _insert_session(conn, "sess-skip-3", status="awake", sleep_report=report)

            # Simulate _mark_session_orphan (merges into existing report)
            async with conn.execute(
                "SELECT status, sleep_report FROM session_record WHERE session_id = ?",
                ("sess-skip-3",),
            ) as cur:
                row = await cur.fetchone()

            report_data = json.loads(row[1]) if row[1] else {}
            report_data["previous_status"] = row[0]

            await conn.execute(
                "UPDATE session_record SET status = 'orphan', sleep_report = ? WHERE session_id = ?",
                (json.dumps(report_data), "sess-skip-3"),
            )
            await conn.commit()

            row = await _get_session(conn, "sess-skip-3")
            assert row[0] == "orphan"
            report = json.loads(row[1])
            assert report["previous_status"] == "awake"
            assert report["claimless_skip_count"] == 3
        finally:
            await conn.close()

    @pytest.mark.asyncio
    async def test_claimless_skip_limit_constant(self):
        """Verify the skip limit is set to 3."""
        from cortex.circadian.consolidation.dream_steps import DreamStepsMixin
        assert DreamStepsMixin._CLAIMLESS_SKIP_LIMIT == 3
