"""Tests for the analysis framework."""

from __future__ import annotations

from dataclasses import FrozenInstanceError
from pathlib import Path
from unittest.mock import MagicMock

import numpy as np
import pytest

from analyzer.analyzer import Analyzer, DummyAnalyzer
from analyzer.context import AnalysisContext
from analyzer.exceptions import AnalyzerExecutionError, AnalyzerRegistrationError
from analyzer.feature import Feature, FeatureSet
from analyzer.pipeline import AnalysisPipeline
from analyzer.registry import AnalyzerRegistry
from analyzer.result import AnalysisResult
from config.settings import Settings
from decoder.decoded_audio import DecodedAudio


def _make_decoded_audio() -> DecodedAudio:
    """Create a minimal DecodedAudio for testing."""
    return DecodedAudio(
        samples=np.zeros((100, 2), dtype=np.float32),
        sample_rate=44100,
        channels=2,
        duration=0.00227,
        bit_depth=16,
    )


def _make_settings(tmp_path: Path) -> Settings:
    """Create test settings."""
    return Settings(
        project_name="Test",
        database_url=f"sqlite:///{tmp_path / 'test.db'}",
        log_level="DEBUG",
        cache_directory=str(tmp_path / "cache"),
        data_directory=str(tmp_path / "data"),
        output_directory=str(tmp_path / "output"),
    )


def _make_context(tmp_path: Path) -> AnalysisContext:
    """Create an AnalysisContext for testing."""
    track = MagicMock()
    track.relative_path = "test.wav"
    return AnalysisContext(
        track=track,
        decoded_audio=_make_decoded_audio(),
        settings=_make_settings(tmp_path),
    )


# --- AnalysisContext tests ---


def test_context_is_frozen(tmp_path: Path) -> None:
    """Verify that AnalysisContext is immutable."""
    context = _make_context(tmp_path)
    with pytest.raises(FrozenInstanceError):
        context.track = MagicMock()  # type: ignore[misc]


# --- Feature tests ---


def test_feature_fields() -> None:
    """Verify that Feature stores all fields correctly."""
    feature = Feature(
        name="brightness",
        value=0.82,
        unit="normalized",
        analyzer="spectral",
        version="1.0",
    )
    assert feature.name == "brightness"
    assert feature.value == 0.82
    assert feature.unit == "normalized"
    assert feature.analyzer == "spectral"
    assert feature.version == "1.0"


def test_feature_defaults() -> None:
    """Verify Feature default values."""
    feature = Feature(name="tempo", value=120.0)
    assert feature.unit is None
    assert feature.analyzer == ""
    assert feature.version == ""


def test_feature_is_frozen() -> None:
    """Verify that Feature is immutable."""
    feature = Feature(name="test", value=1.0)
    with pytest.raises(FrozenInstanceError):
        feature.value = 2.0  # type: ignore[misc]


# --- FeatureSet tests ---


def test_feature_set_iteration() -> None:
    """Verify that FeatureSet iterates over features."""
    f1 = Feature(name="a", value=1)
    f2 = Feature(name="b", value=2)
    fs = FeatureSet([f1, f2])
    features = list(fs)
    assert len(features) == 2
    assert features[0] is f1
    assert features[1] is f2


def test_feature_set_len() -> None:
    """Verify FeatureSet length."""
    fs = FeatureSet([Feature(name="a", value=1), Feature(name="b", value=2)])
    assert len(fs) == 2


def test_feature_set_empty() -> None:
    """Verify empty FeatureSet."""
    fs = FeatureSet()
    assert len(fs) == 0
    assert list(fs) == []


def test_feature_set_find_by_name() -> None:
    """Verify lookup by name returns correct feature or None."""
    f1 = Feature(name="brightness", value=0.8)
    f2 = Feature(name="tempo", value=120.0)
    fs = FeatureSet([f1, f2])

    found = fs.find_by_name("brightness")
    assert found is f1

    not_found = fs.find_by_name("nonexistent")
    assert not_found is None


def test_feature_set_find_all_by_analyzer() -> None:
    """Verify filtering by analyzer name."""
    f1 = Feature(name="a", value=1, analyzer="dsp")
    f2 = Feature(name="b", value=2, analyzer="dsp")
    f3 = Feature(name="c", value=3, analyzer="dna")
    fs = FeatureSet([f1, f2, f3])

    dsp_features = fs.find_all_by_analyzer("dsp")
    assert len(dsp_features) == 2
    assert f1 in dsp_features
    assert f2 in dsp_features

    dna_features = fs.find_all_by_analyzer("dna")
    assert len(dna_features) == 1
    assert f3 in dna_features


def test_feature_set_is_frozen() -> None:
    """Verify that FeatureSet is immutable."""
    fs = FeatureSet([Feature(name="a", value=1)])
    with pytest.raises(FrozenInstanceError):
        fs.features = ()  # type: ignore[misc]


# --- AnalysisResult tests ---


def test_analysis_result_is_frozen() -> None:
    """Verify that AnalysisResult is immutable."""
    result = AnalysisResult(
        analyzer_name="dummy",
        analyzer_version="0.1",
        execution_time_ms=1.0,
        success=True,
        warnings=(),
        feature_set=FeatureSet(),
    )
    with pytest.raises(FrozenInstanceError):
        result.success = False  # type: ignore[misc]


# --- DummyAnalyzer tests ---


def test_dummy_analyzer_returns_feature(tmp_path: Path) -> None:
    """Verify DummyAnalyzer produces framework.version=0.1."""
    context = _make_context(tmp_path)
    analyzer = DummyAnalyzer()
    result = analyzer.analyze(context)

    assert result.analyzer_name == "dummy"
    assert result.analyzer_version == "0.1"
    assert result.success is True

    feature = result.feature_set.find_by_name("framework.version")
    assert feature is not None
    assert feature.value == "0.1"
    assert feature.analyzer == "dummy"
    assert feature.version == "0.1"


def test_dummy_analyzer_result_success(tmp_path: Path) -> None:
    """Verify DummyAnalyzer result has success=True and non-zero time."""
    context = _make_context(tmp_path)
    analyzer = DummyAnalyzer()
    result = analyzer.analyze(context)

    assert result.success is True
    assert result.execution_time_ms >= 0.0
    assert len(result.warnings) == 0


def test_dummy_analyzer_name_and_version() -> None:
    """Verify DummyAnalyzer has correct name and version."""
    analyzer = DummyAnalyzer()
    assert analyzer.name == "dummy"
    assert analyzer.version == "0.1"


# --- AnalyzerRegistry tests ---


def test_registry_register_and_list() -> None:
    """Verify register and list analyzers in order."""
    registry = AnalyzerRegistry()
    a1 = DummyAnalyzer()
    a2 = DummyAnalyzer()
    a2.name = "dummy2"
    registry.register(a1)
    registry.register(a2)

    analyzers = registry.list_analyzers()
    assert len(analyzers) == 2
    assert analyzers[0] is a1
    assert analyzers[1] is a2


def test_registry_duplicate_rejected() -> None:
    """Verify duplicate name raises AnalyzerRegistrationError."""
    registry = AnalyzerRegistry()
    registry.register(DummyAnalyzer())

    with pytest.raises(AnalyzerRegistrationError):
        registry.register(DummyAnalyzer())


def test_registry_unregister() -> None:
    """Verify unregister removes analyzer."""
    registry = AnalyzerRegistry()
    analyzer = DummyAnalyzer()
    registry.register(analyzer)

    registry.unregister("dummy")
    assert len(registry.list_analyzers()) == 0


def test_registry_unregister_unknown_raises() -> None:
    """Verify unregistering unknown name raises KeyError."""
    registry = AnalyzerRegistry()
    with pytest.raises(KeyError):
        registry.unregister("nonexistent")


def test_registry_get_by_name() -> None:
    """Verify lookup by name returns analyzer."""
    registry = AnalyzerRegistry()
    analyzer = DummyAnalyzer()
    registry.register(analyzer)

    found = registry.get("dummy")
    assert found is analyzer


def test_registry_get_unknown_raises() -> None:
    """Verify unknown name raises KeyError."""
    registry = AnalyzerRegistry()
    with pytest.raises(KeyError):
        registry.get("nonexistent")


# --- AnalysisPipeline tests ---


def test_pipeline_execution_order(tmp_path: Path) -> None:
    """Verify analyzers run in registration order."""
    registry = AnalyzerRegistry()

    order: list[str] = []

    class FirstAnalyzer(Analyzer):
        name = "first"
        version = "1.0"

        def analyze(self, context: AnalysisContext) -> AnalysisResult:
            order.append("first")
            return AnalysisResult(
                analyzer_name="first",
                analyzer_version="1.0",
                execution_time_ms=0.1,
                success=True,
                warnings=(),
                feature_set=FeatureSet(),
            )

    class SecondAnalyzer(Analyzer):
        name = "second"
        version = "1.0"

        def analyze(self, context: AnalysisContext) -> AnalysisResult:
            order.append("second")
            return AnalysisResult(
                analyzer_name="second",
                analyzer_version="1.0",
                execution_time_ms=0.1,
                success=True,
                warnings=(),
                feature_set=FeatureSet(),
            )

    registry.register(FirstAnalyzer())
    registry.register(SecondAnalyzer())

    pipeline = AnalysisPipeline(registry)
    context = _make_context(tmp_path)
    pipeline.run(context)

    assert order == ["first", "second"]


def test_pipeline_failure_isolation(tmp_path: Path) -> None:
    """Verify failing analyzer doesn't crash pipeline."""
    registry = AnalyzerRegistry()

    class FailingAnalyzer(Analyzer):
        name = "failing"
        version = "1.0"

        def analyze(self, context: AnalysisContext) -> AnalysisResult:
            raise AnalyzerExecutionError("Intentional failure")

    class GoodAnalyzer(Analyzer):
        name = "good"
        version = "1.0"

        def analyze(self, context: AnalysisContext) -> AnalysisResult:
            return AnalysisResult(
                analyzer_name="good",
                analyzer_version="1.0",
                execution_time_ms=0.1,
                success=True,
                warnings=(),
                feature_set=FeatureSet([Feature(name="ok", value=1)]),
            )

    registry.register(FailingAnalyzer())
    registry.register(GoodAnalyzer())

    pipeline = AnalysisPipeline(registry)
    context = _make_context(tmp_path)
    results = pipeline.run(context)

    assert len(results) == 2
    assert results[0].success is False
    assert results[0].analyzer_name == "failing"
    assert len(results[0].warnings) == 1
    assert results[1].success is True
    assert results[1].analyzer_name == "good"


def test_pipeline_result_aggregation(tmp_path: Path) -> None:
    """Verify pipeline returns one result per analyzer."""
    registry = AnalyzerRegistry()
    registry.register(DummyAnalyzer())

    pipeline = AnalysisPipeline(registry)
    context = _make_context(tmp_path)
    results = pipeline.run(context)

    assert len(results) == 1
    assert results[0].analyzer_name == "dummy"
    assert len(results[0].feature_set) == 1


def test_pipeline_empty_registry(tmp_path: Path) -> None:
    """Verify pipeline with no analyzers returns empty list."""
    registry = AnalyzerRegistry()
    pipeline = AnalysisPipeline(registry)
    context = _make_context(tmp_path)
    results = pipeline.run(context)

    assert len(results) == 0


def test_pipeline_all_fail(tmp_path: Path) -> None:
    """Verify pipeline survives all analyzers failing."""
    registry = AnalyzerRegistry()

    class FailOne(Analyzer):
        name = "fail1"
        version = "1.0"

        def analyze(self, context: AnalysisContext) -> AnalysisResult:
            raise AnalyzerExecutionError("fail1")

    class FailTwo(Analyzer):
        name = "fail2"
        version = "1.0"

        def analyze(self, context: AnalysisContext) -> AnalysisResult:
            raise AnalyzerExecutionError("fail2")

    registry.register(FailOne())
    registry.register(FailTwo())

    pipeline = AnalysisPipeline(registry)
    context = _make_context(tmp_path)
    results = pipeline.run(context)

    assert len(results) == 2
    assert all(not r.success for r in results)


def test_analyzer_is_abstract() -> None:
    """Verify Analyzer cannot be instantiated directly."""
    with pytest.raises(TypeError):
        Analyzer()  # type: ignore[abstract]
