"""Tests for autodebug_replay/transcripts.py TranscriptIndex class."""

from __future__ import annotations

import pytest

from autodebug_replay.transcripts import ReplayDivergence, TranscriptIndex


def _make_record(record_type: str, seq: int, **extra) -> dict:
    """Build a minimal transcript record."""
    rec = {"type": record_type, "seq": seq}
    rec.update(extra)
    return rec


# ---------------------------------------------------------------------------
# test_type_index_built_at_construction
# ---------------------------------------------------------------------------


class TestTypeIndexBuiltAtConstruction:
    """Verify _type_index is populated during __post_init__."""

    def test_single_type(self):
        records = [_make_record("HTTP", 0), _make_record("HTTP", 1)]
        idx = TranscriptIndex(records=records)
        assert "HTTP" in idx._type_index
        assert len(idx._type_index["HTTP"]) == 2

    def test_multiple_types(self):
        records = [
            _make_record("HTTP", 0),
            _make_record("SQL", 1),
            _make_record("HTTP", 2),
            _make_record("REDIS", 3),
        ]
        idx = TranscriptIndex(records=records)
        assert set(idx._type_index.keys()) == {"HTTP", "SQL", "REDIS"}
        assert len(idx._type_index["HTTP"]) == 2
        assert len(idx._type_index["SQL"]) == 1
        assert len(idx._type_index["REDIS"]) == 1

    def test_empty_records(self):
        idx = TranscriptIndex(records=[])
        assert idx._type_index == {}

    def test_records_without_type_field_excluded(self):
        records = [{"seq": 0, "data": "no type"}]
        idx = TranscriptIndex(records=records)
        # None is falsy, so records without a "type" key are skipped
        assert len(idx._type_index) == 0


# ---------------------------------------------------------------------------
# test_expect_returns_correct_record
# ---------------------------------------------------------------------------


class TestExpectReturnsCorrectRecord:
    """Basic expect() returns records in order."""

    def test_first_expect(self):
        records = [_make_record("HTTP", 0, url="a"), _make_record("HTTP", 1, url="b")]
        idx = TranscriptIndex(records=records)
        result = idx.expect("HTTP")
        assert result["seq"] == 0
        assert result["url"] == "a"

    def test_sequential_expect(self):
        records = [_make_record("HTTP", 0, url="a"), _make_record("HTTP", 1, url="b")]
        idx = TranscriptIndex(records=records)
        first = idx.expect("HTTP")
        second = idx.expect("HTTP")
        assert first["seq"] == 0
        assert second["seq"] == 1

    def test_expect_advances_position(self):
        records = [_make_record("SQL", 0), _make_record("SQL", 1), _make_record("SQL", 2)]
        idx = TranscriptIndex(records=records)
        assert idx.positions.get("SQL", 0) == 0
        idx.expect("SQL")
        assert idx.positions["SQL"] == 1
        idx.expect("SQL")
        assert idx.positions["SQL"] == 2


# ---------------------------------------------------------------------------
# test_expect_exhausted_raises_missing_dep
# ---------------------------------------------------------------------------


class TestExpectExhaustedRaisesMissingDep:
    """Going past the end raises MISSING_DEP."""

    def test_single_record_exhausted(self):
        records = [_make_record("HTTP", 0)]
        idx = TranscriptIndex(records=records)
        idx.expect("HTTP")  # consume the only record
        with pytest.raises(ReplayDivergence) as exc_info:
            idx.expect("HTTP")
        assert exc_info.value.code == "MISSING_DEP"
        assert exc_info.value.detail["type"] == "HTTP"
        assert exc_info.value.detail["pos"] == 1

    def test_no_records_of_type(self):
        records = [_make_record("SQL", 0)]
        idx = TranscriptIndex(records=records)
        with pytest.raises(ReplayDivergence) as exc_info:
            idx.expect("HTTP")
        assert exc_info.value.code == "MISSING_DEP"
        assert exc_info.value.detail["type"] == "HTTP"
        assert exc_info.value.detail["pos"] == 0

    def test_empty_transcript(self):
        idx = TranscriptIndex(records=[])
        with pytest.raises(ReplayDivergence) as exc_info:
            idx.expect("HTTP")
        assert exc_info.value.code == "MISSING_DEP"


# ---------------------------------------------------------------------------
# test_has_more_returns_true_when_available
# ---------------------------------------------------------------------------


class TestHasMoreReturnsTrueWhenAvailable:
    """has_more checks correctly when records remain."""

    def test_before_any_expect(self):
        records = [_make_record("HTTP", 0)]
        idx = TranscriptIndex(records=records)
        assert idx.has_more("HTTP") is True

    def test_after_partial_consumption(self):
        records = [_make_record("SQL", 0), _make_record("SQL", 1)]
        idx = TranscriptIndex(records=records)
        idx.expect("SQL")
        assert idx.has_more("SQL") is True


# ---------------------------------------------------------------------------
# test_has_more_returns_false_when_exhausted
# ---------------------------------------------------------------------------


class TestHasMoreReturnsFalseWhenExhausted:
    """After consuming all records, has_more returns False."""

    def test_all_consumed(self):
        records = [_make_record("HTTP", 0)]
        idx = TranscriptIndex(records=records)
        idx.expect("HTTP")
        assert idx.has_more("HTTP") is False

    def test_type_not_present(self):
        records = [_make_record("SQL", 0)]
        idx = TranscriptIndex(records=records)
        assert idx.has_more("HTTP") is False

    def test_empty_transcript(self):
        idx = TranscriptIndex(records=[])
        assert idx.has_more("HTTP") is False


# ---------------------------------------------------------------------------
# test_independent_type_counters
# ---------------------------------------------------------------------------


class TestIndependentTypeCounters:
    """HTTP and SQL positions are independent."""

    def test_http_and_sql_independent(self):
        records = [
            _make_record("HTTP", 0, url="a"),
            _make_record("SQL", 1, query="SELECT 1"),
            _make_record("HTTP", 2, url="b"),
            _make_record("SQL", 3, query="SELECT 2"),
        ]
        idx = TranscriptIndex(records=records)

        http1 = idx.expect("HTTP")
        sql1 = idx.expect("SQL")
        http2 = idx.expect("HTTP")
        sql2 = idx.expect("SQL")

        assert http1["url"] == "a"
        assert http2["url"] == "b"
        assert sql1["query"] == "SELECT 1"
        assert sql2["query"] == "SELECT 2"

    def test_consuming_one_type_does_not_affect_other(self):
        records = [
            _make_record("HTTP", 0),
            _make_record("SQL", 1),
            _make_record("HTTP", 2),
        ]
        idx = TranscriptIndex(records=records)

        # Consume both HTTP records
        idx.expect("HTTP")
        idx.expect("HTTP")
        assert idx.has_more("HTTP") is False

        # SQL should still be available
        assert idx.has_more("SQL") is True
        sql = idx.expect("SQL")
        assert sql["seq"] == 1

    def test_three_types_independent(self):
        records = [
            _make_record("HTTP", 0),
            _make_record("SQL", 1),
            _make_record("REDIS", 2),
        ]
        idx = TranscriptIndex(records=records)

        redis = idx.expect("REDIS")
        sql = idx.expect("SQL")
        http = idx.expect("HTTP")

        # All consumed in arbitrary order, each type independent
        assert http["seq"] == 0
        assert sql["seq"] == 1
        assert redis["seq"] == 2


# ---------------------------------------------------------------------------
# test_large_transcript_pre_indexed
# ---------------------------------------------------------------------------


class TestLargeTranscriptPreIndexed:
    """10000 records, verify _type_index keys and fast lookup."""

    def test_10000_records_indexed(self):
        types = ["HTTP", "SQL", "REDIS", "TIME", "RNG"]
        records = [_make_record(types[i % len(types)], i) for i in range(10000)]
        idx = TranscriptIndex(records=records)

        assert set(idx._type_index.keys()) == set(types)
        for t in types:
            assert len(idx._type_index[t]) == 2000

    def test_large_transcript_expect_performance(self):
        """Verify that expect() doesn't degrade with many records."""
        records = [_make_record("HTTP", i) for i in range(10000)]
        idx = TranscriptIndex(records=records)

        # Consume all 10000 records
        for i in range(10000):
            r = idx.expect("HTTP")
            assert r["seq"] == i

        assert idx.has_more("HTTP") is False

    def test_single_type_in_large_mixed_transcript(self):
        """Only 10 SQL records among 9990 HTTP records -- still O(1) per expect."""
        records = [_make_record("HTTP", i) for i in range(9990)]
        records.extend([_make_record("SQL", 9990 + i, query=f"Q{i}") for i in range(10)])
        idx = TranscriptIndex(records=records)

        assert len(idx._type_index["SQL"]) == 10
        for i in range(10):
            r = idx.expect("SQL")
            assert r["query"] == f"Q{i}"
