"""Tests for sltk.db.corpus — CorpusDB and ingestion."""

from __future__ import annotations

import os
import sqlite3
import tempfile
from pathlib import Path

import pytest

from sltk.data.core import Segment, SegmentList
from sltk.db.corpus import CorpusDB
from sltk.io.elan import write_eaf

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


def _make_eaf(
    directory: Path,
    name: str = "test.eaf",
    segments: list[tuple[float, float, str, str]] | None = None,
    author: str = "TestAuthor",
) -> Path:
    """Create a minimal .eaf file with given segments."""
    if segments is None:
        segments = [
            (0.0, 1.0, "HELLO", "Gloss"),
            (1.0, 2.0, "WORLD", "Gloss"),
            (2.0, 3.5, "HELLO", "Gloss"),
            (0.5, 1.5, "GREET", "Translation"),
        ]
    seg_list = SegmentList(
        [Segment(start=s, end=e, label=label, tier=tier) for s, e, label, tier in segments]
    )
    out = directory / name
    write_eaf(seg_list, out, author=author)
    return out


@pytest.fixture
def tmp_dir():
    with tempfile.TemporaryDirectory() as d:
        yield Path(d)


@pytest.fixture
def corpus(tmp_dir):
    db_path = tmp_dir / "corpus.db"
    db = CorpusDB(db_path)
    yield db
    db.close()


@pytest.fixture
def eaf_dir(tmp_dir):
    eaf_dir = tmp_dir / "elan"
    eaf_dir.mkdir()
    return eaf_dir


@pytest.fixture
def sample_eaf(eaf_dir):
    return _make_eaf(eaf_dir)


@pytest.fixture
def populated_corpus(corpus, sample_eaf):
    corpus.ingest_eaf(sample_eaf)
    return corpus


# ---------------------------------------------------------------------------
# Schema & init tests
# ---------------------------------------------------------------------------


class TestCorpusDBInit:
    def test_creates_db_file(self, tmp_dir):
        db_path = tmp_dir / "sub" / "corpus.db"
        db = CorpusDB(db_path)
        assert db_path.exists()
        db.close()

    def test_schema_version_stored(self, corpus):
        conn = corpus._get_conn()
        row = conn.execute("SELECT value FROM meta WHERE key = 'schema_version'").fetchone()
        assert row is not None
        assert row["value"] == "2"

    def test_tables_exist(self, corpus):
        conn = corpus._get_conn()
        tables = conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
        names = {r["name"] for r in tables}
        assert "files" in names
        assert "tiers" in names
        assert "segments" in names
        assert "media_refs" in names
        assert "meta" in names
        assert "file_metadata" in names
        assert "tier_roles" in names

    def test_fts_table_exists(self, corpus):
        conn = corpus._get_conn()
        tables = conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
        names = {r["name"] for r in tables}
        assert "segments_fts" in names

    def test_indexes_exist(self, corpus):
        conn = corpus._get_conn()
        rows = conn.execute("SELECT name FROM sqlite_master WHERE type='index'").fetchall()
        names = {r["name"] for r in rows}
        expected = {
            "idx_seg_label",
            "idx_seg_file",
            "idx_seg_tier",
            "idx_seg_duration",
            "idx_tiers_participant",
        }
        assert expected.issubset(names), f"Missing indexes: {expected - names}"


# ---------------------------------------------------------------------------
# Ingestion tests
# ---------------------------------------------------------------------------


class TestIngestion:
    def test_ingest_eaf(self, corpus, sample_eaf):
        result = corpus.ingest_eaf(sample_eaf)
        assert result is True

        conn = corpus._get_conn()
        files = conn.execute("SELECT * FROM files").fetchall()
        assert len(files) == 1
        assert files[0]["path"] == str(sample_eaf)
        assert files[0]["segment_count"] == 4

    def test_ingest_skips_unchanged(self, corpus, sample_eaf):
        assert corpus.ingest_eaf(sample_eaf) is True
        assert corpus.ingest_eaf(sample_eaf) is False

    def test_ingest_re_ingests_changed(self, corpus, eaf_dir):
        eaf = _make_eaf(eaf_dir)
        corpus.ingest_eaf(eaf)

        # Rewrite the file (changes mtime)
        _make_eaf(
            eaf_dir,
            segments=[
                (0.0, 1.0, "NEW", "Gloss"),
            ],
        )
        result = corpus.ingest_eaf(eaf)
        assert result is True

        conn = corpus._get_conn()
        segs = conn.execute("SELECT * FROM segments").fetchall()
        assert len(segs) == 1
        assert segs[0]["label"] == "NEW"

    def test_ingest_nonexistent_file(self, corpus, tmp_dir):
        result = corpus.ingest_eaf(tmp_dir / "nonexistent.eaf")
        assert result is False

    def test_ingest_stores_tiers(self, corpus, sample_eaf):
        corpus.ingest_eaf(sample_eaf)
        conn = corpus._get_conn()
        tiers = conn.execute("SELECT * FROM tiers").fetchall()
        tier_names = {t["tier_id"] for t in tiers}
        assert "Gloss" in tier_names
        assert "Translation" in tier_names

    def test_ingest_stores_segments(self, corpus, sample_eaf):
        corpus.ingest_eaf(sample_eaf)
        conn = corpus._get_conn()
        segs = conn.execute("SELECT * FROM segments ORDER BY start_ms").fetchall()
        assert len(segs) == 4
        assert segs[0]["label"] == "HELLO"
        assert segs[0]["start_ms"] == 0
        assert segs[0]["end_ms"] == 1000

    def test_generated_columns(self, corpus, sample_eaf):
        corpus.ingest_eaf(sample_eaf)
        conn = corpus._get_conn()
        seg = conn.execute("SELECT * FROM segments WHERE label = 'HELLO' LIMIT 1").fetchone()
        assert seg["duration_ms"] == 1000
        assert seg["label_lower"] == "hello"

    def test_ingest_stores_tier_attributes(self, corpus, eaf_dir):
        """Tier linguistic_type, participant, annotator are stored."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("Gloss_S1")
        # Manually set participant/annotator on the XML tier
        for tier in doc._root.findall("TIER"):
            if tier.get("TIER_ID") == "Gloss_S1":
                tier.set("PARTICIPANT", "Signer1")
                tier.set("ANNOTATOR", "Ann1")
        doc.add_segment("Gloss_S1", 0.0, 1.0, "HELLO")
        eaf = eaf_dir / "attrs.eaf"
        doc.save(eaf)

        corpus.ingest_eaf(eaf)
        conn = corpus._get_conn()
        tier = conn.execute("SELECT * FROM tiers WHERE tier_id = 'Gloss_S1'").fetchone()
        assert tier["linguistic_type"] == "default-lt"
        assert tier["participant"] == "Signer1"
        assert tier["annotator"] == "Ann1"

    def test_ingest_stores_annotation_ids(self, corpus, sample_eaf):
        """Each segment has a non-empty annotation_id."""
        corpus.ingest_eaf(sample_eaf)
        conn = corpus._get_conn()
        segs = conn.execute("SELECT annotation_id FROM segments").fetchall()
        ids = [s["annotation_id"] for s in segs]
        assert all(
            aid and aid.startswith("a") for aid in ids
        ), f"Expected annotation IDs starting with 'a', got {ids}"
        assert len(set(ids)) == len(ids), "Annotation IDs must be unique"

    def test_ingest_corrupt_eaf(self, corpus, eaf_dir):
        """Corrupt/invalid XML should be skipped gracefully."""
        bad = eaf_dir / "corrupt.eaf"
        bad.write_text("<<<NOT VALID XML>>>")
        result = corpus.ingest_eaf(bad)
        assert result is False


# ---------------------------------------------------------------------------
# Remove & rebuild tests
# ---------------------------------------------------------------------------


class TestRemoveAndRebuild:
    def test_remove_file(self, populated_corpus, sample_eaf):
        populated_corpus.remove_file(sample_eaf)
        conn = populated_corpus._get_conn()
        assert conn.execute("SELECT count(*) FROM files").fetchone()[0] == 0
        assert conn.execute("SELECT count(*) FROM segments").fetchone()[0] == 0

    def test_rebuild(self, corpus, eaf_dir):
        _make_eaf(eaf_dir, "a.eaf")
        _make_eaf(eaf_dir, "b.eaf")
        count = corpus.rebuild(eaf_dir)
        assert count == 2
        conn = corpus._get_conn()
        assert conn.execute("SELECT count(*) FROM files").fetchone()[0] == 2

    def test_rebuild_clears_old_data(self, populated_corpus, eaf_dir):
        # Rebuild should clear old data first
        _make_eaf(eaf_dir, "new.eaf")
        count = populated_corpus.rebuild(eaf_dir)
        # eaf_dir has test.eaf + new.eaf
        assert count == 2

    def test_remove_cascades_to_all_children(self, corpus, eaf_dir):
        """remove_file must cascade-delete tiers, segments, and media_refs."""
        eaf = _make_eaf(eaf_dir)
        corpus.ingest_eaf(eaf)

        conn = corpus._get_conn()
        assert conn.execute("SELECT count(*) FROM tiers").fetchone()[0] > 0
        assert conn.execute("SELECT count(*) FROM segments").fetchone()[0] > 0

        corpus.remove_file(eaf)

        assert conn.execute("SELECT count(*) FROM tiers").fetchone()[0] == 0
        assert conn.execute("SELECT count(*) FROM segments").fetchone()[0] == 0
        assert conn.execute("SELECT count(*) FROM media_refs").fetchone()[0] == 0

    def test_rebuild_empty_directory(self, corpus, tmp_dir):
        """Rebuild on a directory with no .eaf files returns 0."""
        empty_dir = tmp_dir / "empty"
        empty_dir.mkdir()
        count = corpus.rebuild(empty_dir)
        assert count == 0


# ---------------------------------------------------------------------------
# Query tests
# ---------------------------------------------------------------------------


class TestQueries:
    def test_corpus_summary(self, populated_corpus):
        summary = populated_corpus.corpus_summary()
        assert summary["total_files"] == 1
        assert summary["total_segments"] == 4
        assert summary["vocabulary_size"] == 2  # HELLO, WORLD (GREET is on Translation tier)

    def test_status(self, populated_corpus):
        status = populated_corpus.status()
        assert status["file_count"] == 1
        assert status["segment_count"] == 4
        assert status["last_updated"] is not None

    def test_top_glosses(self, populated_corpus):
        glosses = populated_corpus.top_glosses(10)
        assert len(glosses) > 0
        # HELLO appears twice
        hello = next(g for g in glosses if g["gloss"] == "hello")
        assert hello["frequency"] == 2

    def test_find_gloss(self, populated_corpus):
        results = populated_corpus.find_gloss("HELLO")
        assert len(results) == 2
        for r in results:
            assert r["label"] == "HELLO"
            assert "file_path" in r
            assert "tier" in r

    def test_find_gloss_case_insensitive(self, populated_corpus):
        r1 = populated_corpus.find_gloss("hello")
        r2 = populated_corpus.find_gloss("HELLO")
        assert len(r1) == len(r2) == 2

    def test_find_gloss_not_found(self, populated_corpus):
        results = populated_corpus.find_gloss("NONEXISTENT")
        assert len(results) == 0

    def test_gloss_duration_stats(self, populated_corpus):
        stats = populated_corpus.gloss_duration_stats("HELLO")
        assert stats["count"] == 2
        assert stats["min_ms"] == 1000
        assert stats["max_ms"] == 1500
        assert stats["mean_ms"] > 0

    def test_gloss_duration_stats_empty(self, populated_corpus):
        stats = populated_corpus.gloss_duration_stats("NONEXISTENT")
        assert stats["count"] == 0

    def test_search_glosses(self, populated_corpus):
        results = populated_corpus.search_glosses("HELLO")
        assert len(results) == 2

    def test_concordance(self, populated_corpus):
        results = populated_corpus.concordance("WORLD", context_window=5)
        assert len(results) == 1
        r = results[0]
        assert r["target"] == "WORLD"
        assert isinstance(r["left"], list)
        assert isinstance(r["right"], list)

    def test_ngram_frequency(self, populated_corpus):
        ngrams = populated_corpus.ngram_frequency(n=2, limit=10)
        assert len(ngrams) > 0
        for ng in ngrams:
            assert len(ng["ngram"]) == 2
            assert ng["frequency"] >= 1

    def test_cooccurrence(self, populated_corpus):
        results = populated_corpus.cooccurrence("HELLO", window=3, limit=10)
        assert len(results) > 0
        labels = {r["label"] for r in results}
        assert "WORLD" in labels

    def test_find_gloss_by_signer(self, corpus, eaf_dir):
        """find_gloss with signer_id filters by participant."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("Gloss_S1")
        doc.add_tier("Gloss_S2")
        for tier in doc._root.findall("TIER"):
            tid = tier.get("TIER_ID", "")
            if tid == "Gloss_S1":
                tier.set("PARTICIPANT", "Signer1")
            elif tid == "Gloss_S2":
                tier.set("PARTICIPANT", "Signer2")
        doc.add_segment("Gloss_S1", 0.0, 1.0, "HELLO")
        doc.add_segment("Gloss_S1", 1.5, 2.5, "WORLD")
        doc.add_segment("Gloss_S2", 0.0, 1.5, "HELLO")
        eaf = eaf_dir / "signers.eaf"
        doc.save(eaf)

        corpus.ingest_eaf(eaf)

        # Without filter: all 2 HELLO
        all_hello = corpus.find_gloss("HELLO")
        assert len(all_hello) == 2

        # With signer_id filter
        s1 = corpus.find_gloss("HELLO", signer_id="Signer1")
        assert len(s1) == 1
        assert s1[0]["participant"] == "Signer1"

        s2 = corpus.find_gloss("HELLO", signer_id="Signer2")
        assert len(s2) == 1
        assert s2[0]["participant"] == "Signer2"

        # Non-existent signer
        none = corpus.find_gloss("HELLO", signer_id="NoSuchSigner")
        assert len(none) == 0

    def test_compare_gloss_duration_by_signer(self, corpus, eaf_dir):
        """compare_gloss_duration_by_signer returns per-signer stats."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("S1")
        doc.add_tier("S2")
        for tier in doc._root.findall("TIER"):
            tid = tier.get("TIER_ID", "")
            if tid == "S1":
                tier.set("PARTICIPANT", "Signer1")
            elif tid == "S2":
                tier.set("PARTICIPANT", "Signer2")
        doc.add_segment("S1", 0.0, 1.0, "HELLO")
        doc.add_segment("S1", 2.0, 3.0, "HELLO")
        doc.add_segment("S2", 0.0, 2.0, "HELLO")
        eaf = eaf_dir / "cmp_signers.eaf"
        doc.save(eaf)

        corpus.ingest_eaf(eaf)
        results = corpus.compare_gloss_duration_by_signer("HELLO")
        assert len(results) == 2

        by_participant = {r["participant"]: r for r in results}
        assert by_participant["Signer1"]["count"] == 2
        assert by_participant["Signer1"]["mean_ms"] == 1000.0
        assert by_participant["Signer2"]["count"] == 1
        assert by_participant["Signer2"]["mean_ms"] == 2000.0

    def test_gloss_duration_stats_by_signer(self, corpus, eaf_dir):
        """gloss_duration_stats with signer_id param."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("G")
        for tier in doc._root.findall("TIER"):
            tier.set("PARTICIPANT", "SignerA")
        doc.add_segment("G", 0.0, 1.0, "TEST")
        doc.add_segment("G", 2.0, 3.5, "TEST")
        eaf = eaf_dir / "signer_stats.eaf"
        doc.save(eaf)
        corpus.ingest_eaf(eaf)

        stats = corpus.gloss_duration_stats("TEST", signer_id="SignerA")
        assert stats["count"] == 2
        assert stats["min_ms"] == 1000
        assert stats["max_ms"] == 1500

        # Non-existent signer returns zero
        stats2 = corpus.gloss_duration_stats("TEST", signer_id="NoOne")
        assert stats2["count"] == 0

    def test_concordance_context_content(self, populated_corpus):
        """Concordance KWIC has correct left/right context words."""
        results = populated_corpus.concordance("WORLD", context_window=5)
        assert len(results) >= 1
        r = results[0]
        assert r["target"] == "WORLD"
        # WORLD is preceded by HELLO (start_ms=0-1000, WORLD=1000-2000)
        # (exact context depends on tier ordering)
        assert isinstance(r["left"], list)
        assert isinstance(r["right"], list)
        assert r["start_ms"] == 1000
        assert r["end_ms"] == 2000


# ---------------------------------------------------------------------------
# Cross-workspace comparison
# ---------------------------------------------------------------------------


class TestCrossWorkspace:
    def test_compare_gloss_across_dbs(self, tmp_dir, eaf_dir):
        eaf = _make_eaf(eaf_dir)

        db1_path = tmp_dir / "ws1" / "corpus.db"
        db2_path = tmp_dir / "ws2" / "corpus.db"

        db1 = CorpusDB(db1_path)
        db2 = CorpusDB(db2_path)

        db1.ingest_eaf(eaf)
        db2.ingest_eaf(eaf)
        db1.close()
        db2.close()

        results = CorpusDB.compare_gloss_across_dbs("HELLO", [db1_path, db2_path])
        assert len(results) == 2
        for r in results:
            assert r["count"] == 2

    def test_compare_skips_missing_db(self, tmp_dir, eaf_dir):
        eaf = _make_eaf(eaf_dir)
        db1_path = tmp_dir / "ws1" / "corpus.db"
        db1 = CorpusDB(db1_path)
        db1.ingest_eaf(eaf)
        db1.close()

        results = CorpusDB.compare_gloss_across_dbs(
            "HELLO",
            [db1_path, tmp_dir / "nonexistent" / "corpus.db"],
        )
        assert len(results) == 1


# ---------------------------------------------------------------------------
# FTS trigger tests
# ---------------------------------------------------------------------------


class TestFTSTriggers:
    def test_fts_insert(self, populated_corpus):
        conn = populated_corpus._get_conn()
        rows = conn.execute(
            "SELECT * FROM segments_fts WHERE segments_fts MATCH 'hello'"
        ).fetchall()
        assert len(rows) == 2

    def test_fts_delete(self, populated_corpus, sample_eaf):
        populated_corpus.remove_file(sample_eaf)
        conn = populated_corpus._get_conn()
        rows = conn.execute(
            "SELECT * FROM segments_fts WHERE segments_fts MATCH 'hello'"
        ).fetchall()
        assert len(rows) == 0


# ---------------------------------------------------------------------------
# Thread safety
# ---------------------------------------------------------------------------


class TestThreadSafety:
    def test_concurrent_reads(self, populated_corpus):
        import concurrent.futures

        def read_op():
            return populated_corpus.find_gloss("HELLO")

        with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
            futures = [pool.submit(read_op) for _ in range(10)]
            for f in futures:
                result = f.result()
                assert len(result) == 2


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    def test_empty_database_summary(self, corpus):
        """corpus_summary on empty DB returns zero counts."""
        summary = corpus.corpus_summary()
        assert summary["total_files"] == 0
        assert summary["total_segments"] == 0
        assert summary["vocabulary_size"] == 0
        assert summary["signer_count"] == 0
        assert summary["total_tiers"] == 0

    def test_empty_database_status(self, corpus):
        """status on empty DB returns zero counts."""
        status = corpus.status()
        assert status["file_count"] == 0
        assert status["segment_count"] == 0
        assert status["last_updated"] is None

    def test_empty_database_top_glosses(self, corpus):
        """top_glosses on empty DB returns empty list."""
        glosses = corpus.top_glosses(10)
        assert glosses == []

    def test_empty_database_find_gloss(self, corpus):
        """find_gloss on empty DB returns empty list."""
        results = corpus.find_gloss("ANYTHING")
        assert results == []

    def test_empty_database_concordance(self, corpus):
        """concordance on empty DB returns empty list."""
        results = corpus.concordance("ANYTHING")
        assert results == []

    def test_empty_database_ngrams(self, corpus):
        """ngram_frequency on empty DB returns empty list."""
        results = corpus.ngram_frequency(n=2)
        assert results == []

    def test_empty_database_cooccurrence(self, corpus):
        """cooccurrence on empty DB returns empty list."""
        results = corpus.cooccurrence("ANYTHING")
        assert results == []

    def test_unicode_labels(self, corpus, eaf_dir):
        """Unicode characters in labels are stored and queried correctly."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("Gloss")
        doc.add_segment("Gloss", 0.0, 1.0, "\u00e9\u00e0\u00fc")  # éàü
        doc.add_segment("Gloss", 1.0, 2.0, "\u4e16\u754c")  # 世界
        doc.add_segment("Gloss", 2.0, 3.0, "\U0001f44b")  # 👋
        eaf = eaf_dir / "unicode.eaf"
        doc.save(eaf)

        corpus.ingest_eaf(eaf)
        summary = corpus.corpus_summary()
        assert summary["total_segments"] == 3
        assert summary["vocabulary_size"] == 3

        r1 = corpus.find_gloss("\u00e9\u00e0\u00fc")
        assert len(r1) == 1
        assert r1[0]["label"] == "\u00e9\u00e0\u00fc"

        r2 = corpus.find_gloss("\u4e16\u754c")
        assert len(r2) == 1

    def test_long_labels(self, corpus, eaf_dir):
        """Very long label strings are stored without truncation."""
        from sltk.io.elan_roundtrip import ElanDocument

        long_label = "A" * 5000
        doc = ElanDocument.new()
        doc.add_tier("G")
        doc.add_segment("G", 0.0, 1.0, long_label)
        eaf = eaf_dir / "long.eaf"
        doc.save(eaf)

        corpus.ingest_eaf(eaf)
        results = corpus.find_gloss(long_label)
        assert len(results) == 1
        assert results[0]["label"] == long_label

    def test_zero_duration_segments(self, corpus, eaf_dir):
        """Segments with start == end (zero duration) are stored."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("G")
        doc.add_segment("G", 1.0, 1.0, "ZERO")
        eaf = eaf_dir / "zero_dur.eaf"
        doc.save(eaf)

        corpus.ingest_eaf(eaf)
        conn = corpus._get_conn()
        seg = conn.execute("SELECT * FROM segments WHERE label = 'ZERO'").fetchone()
        assert seg is not None
        assert seg["duration_ms"] == 0

        stats = corpus.gloss_duration_stats("ZERO")
        assert stats["count"] == 1
        assert stats["min_ms"] == 0
        assert stats["max_ms"] == 0


# ---------------------------------------------------------------------------
# Vocabulary stats tests
# ---------------------------------------------------------------------------


class TestVocabularyStats:
    def test_vocabulary_stats(self, populated_corpus):
        result = populated_corpus.vocabulary_stats(500)
        assert result["vocabulary_size"] == 2  # HELLO, WORLD (GREET is on Translation tier)
        assert result["total_tokens"] == 3  # 3 gloss-tier segments
        assert "hello" in result["frequencies"]
        assert result["frequencies"]["hello"] == 2
        assert result["frequencies"]["world"] == 1

    def test_vocabulary_stats_empty(self, corpus):
        result = corpus.vocabulary_stats()
        assert result["vocabulary_size"] == 0
        assert result["total_tokens"] == 0
        assert result["frequencies"] == {}

    def test_vocabulary_stats_limit(self, corpus, eaf_dir):
        """Limit parameter restricts the number of glosses returned."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("Gloss")
        for i in range(10):
            doc.add_segment("Gloss", float(i), float(i + 1), f"GLOSS_{i:02d}")
        eaf = eaf_dir / "many.eaf"
        doc.save(eaf)
        corpus.ingest_eaf(eaf)

        result = corpus.vocabulary_stats(limit=5)
        assert len(result["frequencies"]) == 5
        assert result["total_tokens"] == 10


# ---------------------------------------------------------------------------
# Co-occurrence matrix tests
# ---------------------------------------------------------------------------


class TestCooccurrenceMatrix:
    def test_cooccurrence_matrix(self, populated_corpus):
        result = populated_corpus.cooccurrence_matrix(top_n=10, window=2)
        assert isinstance(result["glosses"], list)
        assert isinstance(result["matrix"], list)
        assert isinstance(result["frequencies"], dict)
        # Matrix is square
        n = len(result["glosses"])
        assert len(result["matrix"]) == n
        for row in result["matrix"]:
            assert len(row) == n

    def test_cooccurrence_matrix_empty(self, corpus):
        result = corpus.cooccurrence_matrix()
        assert result["glosses"] == []
        assert result["matrix"] == []
        assert result["frequencies"] == {}

    def test_cooccurrence_matrix_values(self, corpus, eaf_dir):
        """Adjacent glosses should have non-zero co-occurrence."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("Gloss")
        doc.add_segment("Gloss", 0.0, 1.0, "A")
        doc.add_segment("Gloss", 1.0, 2.0, "B")
        doc.add_segment("Gloss", 2.0, 3.0, "A")
        doc.add_segment("Gloss", 3.0, 4.0, "B")
        eaf = eaf_dir / "matrix.eaf"
        doc.save(eaf)
        corpus.ingest_eaf(eaf)

        result = corpus.cooccurrence_matrix(top_n=10, window=1)
        glosses = result["glosses"]
        matrix = result["matrix"]
        assert len(glosses) == 2
        # A and B should co-occur
        a_idx = glosses.index("a")
        b_idx = glosses.index("b")
        assert matrix[a_idx][b_idx] > 0
        assert matrix[b_idx][a_idx] > 0


# ---------------------------------------------------------------------------
# Duration analysis tests
# ---------------------------------------------------------------------------


class TestAllDurationStats:
    def test_all_duration_stats(self, corpus, eaf_dir):
        """Returns per-gloss duration aggregates."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("Gloss")
        # SIGN1 appears 5 times, SIGN2 appears 3 times
        for i in range(5):
            doc.add_segment("Gloss", float(i), float(i) + 0.5, "SIGN1")
        for i in range(5, 8):
            doc.add_segment("Gloss", float(i), float(i) + 1.0, "SIGN2")
        eaf = eaf_dir / "durations.eaf"
        doc.save(eaf)
        corpus.ingest_eaf(eaf)

        result = corpus.all_duration_stats(limit=50, min_count=3)
        # SIGN1 has 5 >= 3, SIGN2 has 3 >= 3
        assert len(result) == 2
        by_gloss = {r["gloss"]: r for r in result}
        # Glosses are stored lowercase via generated column
        assert "sign1" in by_gloss
        assert by_gloss["sign1"]["count"] == 5
        assert by_gloss["sign1"]["mean_ms"] == 500.0
        assert by_gloss["sign2"]["count"] == 3
        assert by_gloss["sign2"]["mean_ms"] == 1000.0

    def test_all_duration_stats_min_count(self, populated_corpus):
        """min_count filters out infrequent glosses."""
        # HELLO=2, WORLD=1 (GREET excluded: Translation tier) → with min_count=3 → empty
        result = populated_corpus.all_duration_stats(min_count=3)
        assert len(result) == 0

    def test_all_duration_stats_empty(self, corpus):
        result = corpus.all_duration_stats()
        assert result == []


# ---------------------------------------------------------------------------
# Minimal pairs tests
# ---------------------------------------------------------------------------


class TestMinimalPairs:
    def test_minimal_pairs(self, corpus, eaf_dir):
        """Similar glosses are detected."""
        from sltk.io.elan_roundtrip import ElanDocument

        doc = ElanDocument.new()
        doc.add_tier("Gloss")
        # Create glosses that differ by one character
        for i in range(5):
            doc.add_segment("Gloss", float(i * 3), float(i * 3) + 1.0, "HOUSE")
        for i in range(5):
            doc.add_segment("Gloss", float(i * 3 + 1), float(i * 3) + 2.0, "MOUSE")
        for i in range(5):
            doc.add_segment("Gloss", float(i * 3 + 2), float(i * 3) + 3.0, "TREE")
        eaf = eaf_dir / "pairs.eaf"
        doc.save(eaf)
        corpus.ingest_eaf(eaf)

        result = corpus.minimal_pairs(threshold=0.5, max_pairs=100)
        assert result["vocabulary_size"] >= 3
        assert result["similarity_threshold"] == 0.5
        # HOUSE and MOUSE are similar (differ by 1 char)
        pairs = result["pairs"]
        pair_set = {(p["gloss1"], p["gloss2"]) for p in pairs}
        assert ("house", "mouse") in pair_set or ("mouse", "house") in pair_set

    def test_minimal_pairs_high_threshold(self, populated_corpus):
        """Very high threshold returns no pairs."""
        result = populated_corpus.minimal_pairs(threshold=0.99, max_pairs=50)
        assert result["total_pairs"] == 0
        assert result["pairs"] == []

    def test_minimal_pairs_empty(self, corpus):
        result = corpus.minimal_pairs()
        assert result["vocabulary_size"] == 0
        assert result["pairs"] == []


# ---------------------------------------------------------------------------
# Synthetic / filename-based annotation tests
# ---------------------------------------------------------------------------


class TestIngestSynthetic:
    def test_ingest_synthetic(self, corpus):
        """Creates a synthetic entry with path synthetic://stem and source 'filename'."""
        corpus.ingest_synthetic("/data/BOOK_signer1.mp4", "BOOK", "Gloss")

        conn = corpus._get_conn()
        row = conn.execute(
            "SELECT id, path, stem FROM files WHERE path = ?",
            ("synthetic://BOOK_signer1",),
        ).fetchone()
        assert row is not None
        file_id = row[0]
        assert row[1] == "synthetic://BOOK_signer1"
        assert row[2] == "BOOK_signer1"

        # Check tier
        tier = conn.execute("SELECT tier_id FROM tiers WHERE file_id = ?", (file_id,)).fetchone()
        assert tier is not None

        # Check segment has gloss and source='filename'
        seg = conn.execute(
            "SELECT label, source FROM segments WHERE file_id = ?",
            (file_id,),
        ).fetchone()
        assert seg is not None
        assert seg[0] == "BOOK"
        assert seg[1] == "filename"

    def test_ingest_synthetic_replaces(self, corpus):
        """Re-ingesting replaces the old gloss."""
        corpus.ingest_synthetic("/data/HELLO_cam1.mp4", "HELLO", "Gloss")
        corpus.ingest_synthetic("/data/HELLO_cam1.mp4", "HI", "Gloss")

        conn = corpus._get_conn()
        segs = conn.execute(
            "SELECT label FROM segments WHERE file_id = " "(SELECT id FROM files WHERE path = ?)",
            ("synthetic://HELLO_cam1",),
        ).fetchall()
        assert len(segs) == 1
        assert segs[0][0] == "HI"

    def test_ingest_synthetic_no_conflict(self, corpus, sample_eaf):
        """Synthetic entries coexist with real .eaf entries."""
        corpus.ingest_eaf(sample_eaf)
        corpus.ingest_synthetic("/data/HELLO_cam1.mp4", "HELLO", "Gloss")

        conn = corpus._get_conn()
        files = conn.execute("SELECT path FROM files ORDER BY path").fetchall()
        paths = [r[0] for r in files]
        assert any(p.endswith(".eaf") for p in paths)
        assert "synthetic://HELLO_cam1" in paths

        # Both contribute to vocabulary
        stats = corpus.vocabulary_stats(100)
        assert stats["vocabulary_size"] >= 2

    def test_ingest_synthetic_shows_in_glosses(self, corpus):
        """Synthetic glosses appear in top_glosses."""
        corpus.ingest_synthetic("/data/A.mp4", "APPLE", "Gloss")
        corpus.ingest_synthetic("/data/B.mp4", "APPLE", "Gloss")
        corpus.ingest_synthetic("/data/C.mp4", "BANANA", "Gloss")

        glosses = corpus.top_glosses(10)
        gloss_map = {g["gloss"]: g["frequency"] for g in glosses}
        assert gloss_map["apple"] == 2
        assert gloss_map["banana"] == 1


# ---------------------------------------------------------------------------
# Tier Auto-Detection Tests
# ---------------------------------------------------------------------------


class TestTierAutoDetect:
    """Test the _detect_tier_role classmethod priority-ordered pattern matching."""

    def test_gloss_priority_over_hand(self):
        """RH-IDgloss should be gloss, not right_hand."""
        assert CorpusDB._detect_tier_role("RH-IDgloss") == "gloss"
        assert CorpusDB._detect_tier_role("LH-IDgloss") == "gloss"
        assert CorpusDB._detect_tier_role("IDgloss") == "gloss"
        assert CorpusDB._detect_tier_role("id-gloss") == "gloss"

    def test_nonmanual_tiers(self):
        """Head, Eye&Brow, Mouthing -> nonmanual."""
        assert CorpusDB._detect_tier_role("Head") == "nonmanual"
        assert CorpusDB._detect_tier_role("Eye&Brow") == "nonmanual"
        assert CorpusDB._detect_tier_role("Mouthing") == "nonmanual"
        assert CorpusDB._detect_tier_role("MouthGest") == "nonmanual"
        assert CorpusDB._detect_tier_role("Face") == "nonmanual"
        assert CorpusDB._detect_tier_role("Body") == "nonmanual"
        assert CorpusDB._detect_tier_role("Gaze") == "nonmanual"

    def test_translation_tiers(self):
        """FreeTransl, LitTransl -> translation."""
        assert CorpusDB._detect_tier_role("FreeTransl") == "translation"
        assert CorpusDB._detect_tier_role("LitTransl") == "translation"
        assert CorpusDB._detect_tier_role("Free Translation") == "translation"

    def test_ca_word_boundary(self):
        """CA -> constructed_action, but Backchannel -> discourse (not CA)."""
        assert CorpusDB._detect_tier_role("CA") == "constructed_action"
        assert CorpusDB._detect_tier_role("CA-onset") == "constructed_action"
        assert CorpusDB._detect_tier_role("Constructed Action") == "constructed_action"
        # 'Backchannel' contains 'ca' as substring but should be discourse
        assert CorpusDB._detect_tier_role("Backchannel") == "discourse"

    def test_clause_word_boundary(self):
        """CLU-main -> clause, CLU -> clause."""
        assert CorpusDB._detect_tier_role("CLU-main") == "clause"
        assert CorpusDB._detect_tier_role("CLU") == "clause"
        assert CorpusDB._detect_tier_role("Clause") == "clause"

    def test_hand_tiers(self):
        """RH-Handsh -> right_hand, LH-Loc -> left_hand."""
        assert CorpusDB._detect_tier_role("RH-Handsh") == "right_hand"
        assert CorpusDB._detect_tier_role("RH-Loc") == "right_hand"
        assert CorpusDB._detect_tier_role("LH-Loc") == "left_hand"
        assert CorpusDB._detect_tier_role("LH-Handsh") == "left_hand"
        assert CorpusDB._detect_tier_role("Left Hand") == "left_hand"
        assert CorpusDB._detect_tier_role("Right Hand") == "right_hand"

    def test_discourse_tiers(self):
        """Turn, Question, Backchannel -> discourse."""
        assert CorpusDB._detect_tier_role("Turn") == "discourse"
        assert CorpusDB._detect_tier_role("Question") == "discourse"
        assert CorpusDB._detect_tier_role("Backchannel") == "discourse"
        assert CorpusDB._detect_tier_role("Back Channel") == "discourse"

    def test_metadata_tiers(self):
        """Comments, Interview -> metadata."""
        assert CorpusDB._detect_tier_role("Comments") == "metadata"
        assert CorpusDB._detect_tier_role("Interview") == "metadata"
        assert CorpusDB._detect_tier_role("Stimulus") == "metadata"
        assert CorpusDB._detect_tier_role("Notes") == "metadata"

    def test_unknown_default_other(self):
        """Doubling, Serial Verb -> other (NOT gloss)."""
        assert CorpusDB._detect_tier_role("Doubling") == "other"
        assert CorpusDB._detect_tier_role("Serial Verb") == "other"
        assert CorpusDB._detect_tier_role("SomeRandomTier") == "other"
