"""
Tests for unified annotation I/O.
"""

import json
import shutil
import tempfile
from pathlib import Path

import pytest

from sltk.data.core import Sample, Segment, SegmentList
from sltk.exceptions import SLTKFileNotFoundError
from sltk.io.annotations import (
    SCHEMA_VERSION,
    TIER_GLOSS,
    TIER_GLOSS_RH,
    TIER_TRANSLATION,
    generate_manifest,
    list_samples,
    read_manifest,
    read_unified_annotation,
    validate_annotation,
    write_manifest,
    write_unified_annotation,
)


@pytest.fixture
def temp_dir():
    """Create a temporary directory for tests."""
    temp_path = Path(tempfile.mkdtemp())
    yield temp_path
    shutil.rmtree(temp_path)


@pytest.fixture
def sample_annotation():
    """Create a sample annotation dict."""
    return {
        "schema_version": SCHEMA_VERSION,
        "sample_id": "test_sample_001",
        "dataset": "test_dataset",
        "video_relative_path": "videos/test.mp4",
        "duration_sec": 10.5,
        "fps": 25.0,
        "signer_id": "signer_01",
        "split": "train",
        "segments": [
            {
                "start": 0.5,
                "end": 1.5,
                "label": "HELLO",
                "tier": "gloss",
            },
            {
                "start": 1.5,
                "end": 2.5,
                "label": "WORLD",
                "tier": "gloss",
            },
        ],
        "glosses": ["HELLO", "WORLD"],
        "text": "Hello world",
        "metadata": {"custom_field": "value"},
        "source": {
            "format": "test",
            "path": "test.json",
            "converted_at": "2026-01-27T12:00:00Z",
        },
    }


@pytest.fixture
def sample_object():
    """Create a Sample object for testing."""
    segments = SegmentList(
        [
            Segment(start=0.5, end=1.5, label="HELLO", tier=TIER_GLOSS),
            Segment(start=1.5, end=2.5, label="WORLD", tier=TIER_GLOSS),
        ]
    )
    return Sample(
        id="test_sample_001",
        segments=segments,
        glosses=["HELLO", "WORLD"],
        text="Hello world",
        signer_id="signer_01",
        dataset="test_dataset",
        split="train",
        metadata={"custom_field": "value"},
    )


class TestWriteUnifiedAnnotation:
    """Tests for write_unified_annotation."""

    def test_write_basic(self, temp_dir, sample_object):
        """Test basic write functionality."""
        output_path = temp_dir / "test.json"
        result = write_unified_annotation(sample_object, output_path)

        assert result == output_path
        assert output_path.exists()

        with open(output_path) as f:
            data = json.load(f)

        assert data["schema_version"] == SCHEMA_VERSION
        assert data["sample_id"] == "test_sample_001"
        assert data["dataset"] == "test_dataset"
        assert data["signer_id"] == "signer_01"
        assert len(data["segments"]) == 2

    def test_write_with_source_info(self, temp_dir, sample_object):
        """Test writing with source provenance info."""
        output_path = temp_dir / "test.json"
        write_unified_annotation(
            sample_object,
            output_path,
            source_format="elan",
            source_path="original.eaf",
        )

        with open(output_path) as f:
            data = json.load(f)

        assert "source" in data
        assert data["source"]["format"] == "elan"
        assert data["source"]["path"] == "original.eaf"
        assert "converted_at" in data["source"]

    def test_write_creates_directory(self, temp_dir, sample_object):
        """Test that write creates parent directories."""
        output_path = temp_dir / "nested" / "dir" / "test.json"
        write_unified_annotation(sample_object, output_path)

        assert output_path.exists()

    def test_write_empty_segments(self, temp_dir):
        """Test writing sample with no segments."""
        sample = Sample(id="empty", dataset="test")
        output_path = temp_dir / "empty.json"
        write_unified_annotation(sample, output_path)

        with open(output_path) as f:
            data = json.load(f)

        assert "segments" not in data or data["segments"] == []


class TestReadUnifiedAnnotation:
    """Tests for read_unified_annotation."""

    def test_read_basic(self, temp_dir, sample_annotation):
        """Test basic read functionality."""
        file_path = temp_dir / "test.json"
        with open(file_path, "w") as f:
            json.dump(sample_annotation, f)

        sample = read_unified_annotation(file_path)

        assert sample.id == "test_sample_001"
        assert sample.dataset == "test_dataset"
        assert sample.signer_id == "signer_01"
        assert sample.split == "train"
        assert sample.glosses == ["HELLO", "WORLD"]
        assert sample.text == "Hello world"

    def test_read_segments(self, temp_dir, sample_annotation):
        """Test reading segments."""
        file_path = temp_dir / "test.json"
        with open(file_path, "w") as f:
            json.dump(sample_annotation, f)

        sample = read_unified_annotation(file_path)

        assert sample.segments is not None
        assert len(sample.segments) == 2
        assert sample.segments[0].start == 0.5
        assert sample.segments[0].end == 1.5
        assert sample.segments[0].label == "HELLO"
        assert sample.segments[0].tier == "gloss"

    def test_read_with_video_path(self, temp_dir, sample_annotation):
        """Test reading with video path resolution."""
        file_path = temp_dir / "test.json"
        with open(file_path, "w") as f:
            json.dump(sample_annotation, f)

        video_root = temp_dir / "video_root"
        video_root.mkdir()

        sample = read_unified_annotation(
            file_path,
            load_video_path=True,
            video_root=video_root,
        )

        assert sample.video_path == video_root / "videos/test.mp4"

    def test_read_file_not_found(self, temp_dir):
        """Test error handling for missing file."""
        with pytest.raises(SLTKFileNotFoundError):
            read_unified_annotation(temp_dir / "nonexistent.json")


class TestRoundTrip:
    """Test round-trip read/write consistency."""

    def test_round_trip_basic(self, temp_dir, sample_object):
        """Test write then read preserves data."""
        file_path = temp_dir / "roundtrip.json"

        write_unified_annotation(sample_object, file_path)
        loaded = read_unified_annotation(file_path)

        assert loaded.id == sample_object.id
        assert loaded.dataset == sample_object.dataset
        assert loaded.signer_id == sample_object.signer_id
        assert loaded.split == sample_object.split
        assert loaded.glosses == sample_object.glosses
        assert loaded.text == sample_object.text

    def test_round_trip_segments(self, temp_dir, sample_object):
        """Test segments preserved in round-trip."""
        file_path = temp_dir / "roundtrip.json"

        write_unified_annotation(sample_object, file_path)
        loaded = read_unified_annotation(file_path)

        assert len(loaded.segments) == len(sample_object.segments)
        for orig, load in zip(sample_object.segments, loaded.segments):
            assert orig.start == load.start
            assert orig.end == load.end
            assert orig.label == load.label
            assert orig.tier == load.tier


class TestValidateAnnotation:
    """Tests for validate_annotation."""

    def test_valid_annotation(self, sample_annotation):
        """Test validation of valid annotation."""
        is_valid, errors = validate_annotation(sample_annotation)

        assert is_valid
        assert len(errors) == 0

    def test_missing_required_fields(self):
        """Test validation catches missing required fields."""
        data = {"schema_version": SCHEMA_VERSION}
        is_valid, errors = validate_annotation(data)

        assert not is_valid
        assert any("Missing required fields" in e for e in errors)

    def test_invalid_schema_version(self, sample_annotation):
        """Test validation catches wrong schema version."""
        sample_annotation["schema_version"] = "0.0.1"
        is_valid, errors = validate_annotation(sample_annotation)

        assert not is_valid
        assert any("schema version" in e.lower() for e in errors)

    def test_invalid_segment(self, sample_annotation):
        """Test validation catches invalid segments."""
        sample_annotation["segments"][0]["end"] = -1  # Invalid
        is_valid, errors = validate_annotation(sample_annotation)

        # Basic validation doesn't catch negative end
        # But end < start is caught
        sample_annotation["segments"][0]["start"] = 5
        sample_annotation["segments"][0]["end"] = 3  # end < start
        is_valid, errors = validate_annotation(sample_annotation)

        assert not is_valid
        assert any("end" in e.lower() for e in errors)

    def test_validate_from_file(self, temp_dir, sample_annotation):
        """Test validation from file path."""
        file_path = temp_dir / "test.json"
        with open(file_path, "w") as f:
            json.dump(sample_annotation, f)

        is_valid, errors = validate_annotation(file_path)
        assert is_valid

    def test_validate_invalid_json(self, temp_dir):
        """Test validation handles invalid JSON."""
        file_path = temp_dir / "invalid.json"
        with open(file_path, "w") as f:
            f.write("not valid json")

        is_valid, errors = validate_annotation(file_path)
        assert not is_valid


class TestManifest:
    """Tests for manifest operations."""

    def test_write_manifest(self, temp_dir):
        """Test manifest writing."""
        samples = [
            {"sample_id": "s1", "split": "train", "num_segments": 5},
            {"sample_id": "s2", "split": "test", "num_segments": 3},
        ]
        output_path = temp_dir / "manifest.json"

        write_manifest(
            dataset="test_dataset",
            samples=samples,
            output_path=output_path,
            source_format="elan",
        )

        assert output_path.exists()

        manifest = read_manifest(output_path)
        assert manifest["dataset"] == "test_dataset"
        assert manifest["source_format"] == "elan"
        assert len(manifest["samples"]) == 2

    def test_generate_manifest(self, temp_dir, sample_annotation):
        """Test manifest generation from annotation files."""
        # Create some annotation files
        for i in range(3):
            ann = sample_annotation.copy()
            ann["sample_id"] = f"sample_{i}"
            ann["split"] = "train" if i < 2 else "test"

            with open(temp_dir / f"sample_{i}.json", "w") as f:
                json.dump(ann, f)

        manifest = generate_manifest(temp_dir, dataset="test_dataset")

        assert manifest["dataset"] == "test_dataset"
        assert len(manifest["samples"]) == 3

        stats = manifest["statistics"]
        assert stats["total_samples"] == 3
        assert stats["total_segments"] == 6  # 2 segments per sample
        assert stats["samples_by_split"]["train"] == 2
        assert stats["samples_by_split"]["test"] == 1

    def test_list_samples(self, temp_dir, sample_annotation):
        """Test listing samples from annotation directory."""
        # Create files
        for i, split in enumerate(["train", "train", "test"]):
            ann = sample_annotation.copy()
            ann["sample_id"] = f"sample_{i}"
            ann["split"] = split

            with open(temp_dir / f"sample_{i}.json", "w") as f:
                json.dump(ann, f)

        # Generate manifest
        generate_manifest(temp_dir, dataset="test")

        # List all samples
        all_samples = list_samples(temp_dir)
        assert len(all_samples) == 3

        # List by split
        train_samples = list_samples(temp_dir, split="train")
        assert len(train_samples) == 2

        test_samples = list_samples(temp_dir, split="test")
        assert len(test_samples) == 1


class TestTierNames:
    """Test standard tier name handling."""

    def test_standard_tiers_preserved(self, temp_dir):
        """Test that standard tier names are preserved."""
        sample = Sample(
            id="test",
            dataset="test",
            segments=SegmentList(
                [
                    Segment(0, 1, "A", tier=TIER_GLOSS),
                    Segment(1, 2, "B", tier=TIER_GLOSS_RH),
                    Segment(2, 3, "translation", tier=TIER_TRANSLATION),
                ]
            ),
        )

        file_path = temp_dir / "test.json"
        write_unified_annotation(sample, file_path)
        loaded = read_unified_annotation(file_path)

        tiers = [s.tier for s in loaded.segments]
        assert TIER_GLOSS in tiers
        assert TIER_GLOSS_RH in tiers
        assert TIER_TRANSLATION in tiers


class TestEdgeCases:
    """Test edge cases and error handling."""

    def test_empty_sample(self, temp_dir):
        """Test handling of minimal sample."""
        sample = Sample(id="minimal", dataset="test")
        file_path = temp_dir / "minimal.json"

        write_unified_annotation(sample, file_path)
        loaded = read_unified_annotation(file_path)

        assert loaded.id == "minimal"
        assert loaded.dataset == "test"
        assert loaded.segments is None or len(loaded.segments) == 0

    def test_unicode_content(self, temp_dir):
        """Test Unicode handling in labels and text."""
        sample = Sample(
            id="unicode",
            dataset="test",
            glosses=["你好", "世界", "こんにちは"],
            text="Hello in multiple languages: 你好 こんにちは",
        )
        file_path = temp_dir / "unicode.json"

        write_unified_annotation(sample, file_path)
        loaded = read_unified_annotation(file_path)

        assert loaded.glosses == ["你好", "世界", "こんにちは"]
        assert "你好" in loaded.text

    def test_special_characters_in_labels(self, temp_dir):
        """Test special characters in segment labels."""
        sample = Sample(
            id="special",
            dataset="test",
            segments=SegmentList(
                [
                    Segment(0, 1, "G:WELL", tier=TIER_GLOSS),
                    Segment(1, 2, "PT:PRO1SG", tier=TIER_GLOSS),
                    Segment(2, 3, "GROW-UP(FALSE-START)", tier=TIER_GLOSS),
                ]
            ),
        )
        file_path = temp_dir / "special.json"

        write_unified_annotation(sample, file_path)
        loaded = read_unified_annotation(file_path)

        labels = [s.label for s in loaded.segments]
        assert "G:WELL" in labels
        assert "PT:PRO1SG" in labels
        assert "GROW-UP(FALSE-START)" in labels

    def test_large_segment_count(self, temp_dir):
        """Test handling of many segments."""
        segments = [
            Segment(i * 0.1, (i + 1) * 0.1, f"SEG{i}", tier=TIER_GLOSS) for i in range(1000)
        ]
        sample = Sample(
            id="large",
            dataset="test",
            segments=SegmentList(segments),
        )
        file_path = temp_dir / "large.json"

        write_unified_annotation(sample, file_path)
        loaded = read_unified_annotation(file_path)

        assert len(loaded.segments) == 1000
