"""
Unit tests for TraceabilityService.

Tests trace_artifact, get_producing_run, get_input_artifacts,
get_generator_runs, and get_validation_history.
"""

import pytest
import tempfile
from datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock

from markitect.prompts.dependencies.models import DependencyEdge, EdgeType
from markitect.prompts.dependencies.repository import SQLiteDependencyRepository
from markitect.prompts.execution.models import (
    ExecutionStage,
    PromptRun,
    RunConfig,
    RunStatus,
)
from markitect.prompts.models import Artifact, ArtifactType
from markitect.prompts.repositories.sqlite import SQLiteArtifactRepository
from markitect.prompts.traceability.models import (
    ArtifactLineage,
    ProvenanceTrace,
    RunSummary,
)
from markitect.prompts.traceability.service import TraceabilityService


@pytest.fixture
def temp_db():
    """Create temporary database for testing."""
    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
        db_path = f.name
    yield db_path
    Path(db_path).unlink(missing_ok=True)


@pytest.fixture
def artifact_repo(temp_db):
    """Create artifact repository."""
    return SQLiteArtifactRepository(temp_db)


@pytest.fixture
def dep_repo(temp_db):
    """Create dependency repository."""
    return SQLiteDependencyRepository(temp_db)


@pytest.fixture
def service(artifact_repo, dep_repo, temp_db):
    """Create TraceabilityService."""
    return TraceabilityService(artifact_repo, dep_repo, db_path=temp_db)


def _make_artifact(repo, space_id, name, content="content", atype=ArtifactType.CONTENT):
    """Helper to create and persist an artifact."""
    artifact = Artifact.create(
        space_id=space_id, name=name, content=content, artifact_type=atype
    )
    return repo.create(artifact)


def _make_run(template_id, status=RunStatus.SUCCESS, parent_run_id=None, depth=0):
    """Helper to create a PromptRun."""
    run = PromptRun.create(
        template_id=template_id,
        input_bundle_hash="hash-abc",
        parent_run_id=parent_run_id,
        depth=depth,
    )
    if status == RunStatus.SUCCESS:
        run.mark_complete()
    return run


def _make_edge(repo, source, target, run_id, edge_type=EdgeType.REQUIRES):
    """Helper to create a dependency edge."""
    edge = DependencyEdge.create(
        source_artifact_id=source,
        target_artifact_id=target,
        run_id=run_id,
        edge_type=edge_type,
    )
    return repo.create(edge)


class TestTraceArtifact:
    """Tests for trace_artifact."""

    def test_trace_returns_provenance(self, service, artifact_repo, dep_repo):
        """Test trace_artifact returns a ProvenanceTrace."""
        art = _make_artifact(artifact_repo, "space-1", "output-doc")
        trace = service.trace_artifact(art.id)

        assert isinstance(trace, ProvenanceTrace)
        assert trace.artifact_id == art.id

    def test_trace_with_producing_run(self, service, artifact_repo, dep_repo):
        """Test trace finds producing run via manifest metadata."""
        template = _make_artifact(
            artifact_repo, "space-1", "tmpl", atype=ArtifactType.TEMPLATE
        )
        output = _make_artifact(artifact_repo, "space-1", "output")

        run = _make_run(template.id)
        run.metadata["manifest"] = {
            "output_artifacts": [{"artifact_id": output.id}],
            "resolved_inputs": [],
        }
        service.register_run(run)

        trace = service.trace_artifact(output.id)
        assert trace.producing_run is not None
        assert trace.producing_run.run_id == run.id

    def test_trace_no_producing_run(self, service, artifact_repo):
        """Test trace with no producing run returns None."""
        art = _make_artifact(artifact_repo, "space-1", "standalone")
        trace = service.trace_artifact(art.id)
        assert trace.producing_run is None

    def test_trace_includes_dependency_chain(self, service, artifact_repo, dep_repo):
        """Test trace includes transitive dependency chain."""
        a = _make_artifact(artifact_repo, "s", "a")
        b = _make_artifact(artifact_repo, "s", "b")
        c = _make_artifact(artifact_repo, "s", "c")

        _make_edge(dep_repo, a.id, b.id, "r1")
        _make_edge(dep_repo, b.id, c.id, "r1")

        trace = service.trace_artifact(a.id)
        assert b.id in trace.dependency_chain
        assert c.id in trace.dependency_chain

    def test_trace_to_dict(self, service, artifact_repo):
        """Test ProvenanceTrace serialization."""
        art = _make_artifact(artifact_repo, "s", "x")
        trace = service.trace_artifact(art.id)
        d = trace.to_dict()
        assert d["artifact_id"] == art.id
        assert "producing_run" in d
        assert "input_artifacts" in d
        assert "dependency_chain" in d


class TestGetProducingRun:
    """Tests for get_producing_run."""

    def test_finds_run_via_generates_edge(self, service, artifact_repo, dep_repo):
        """Test finding run via generates dependency edge."""
        output = _make_artifact(artifact_repo, "s", "output")
        run = _make_run("tmpl-1")
        service.register_run(run)

        # Create a "generates" edge from run.id -> output.id
        _make_edge(dep_repo, run.id, output.id, run.id, EdgeType.GENERATES)

        result = service.get_producing_run(output.id)
        assert result is not None
        assert result.run_id == run.id

    def test_finds_run_via_manifest(self, service, artifact_repo):
        """Test finding run via manifest output_artifacts."""
        output = _make_artifact(artifact_repo, "s", "output")
        run = _make_run("tmpl-1")
        run.metadata["manifest"] = {
            "output_artifacts": [{"artifact_id": output.id}],
        }
        service.register_run(run)

        result = service.get_producing_run(output.id)
        assert result is not None
        assert result.run_id == run.id

    def test_returns_none_when_not_found(self, service):
        """Test returns None when no producing run exists."""
        result = service.get_producing_run("nonexistent")
        assert result is None


class TestGetInputArtifacts:
    """Tests for get_input_artifacts."""

    def test_finds_inputs_via_edges(self, service, artifact_repo, dep_repo):
        """Test finding input artifacts via dependency edges."""
        inp = _make_artifact(artifact_repo, "s", "input-data")
        run = _make_run("tmpl-1")
        service.register_run(run)

        _make_edge(dep_repo, inp.id, run.id, run.id, EdgeType.REQUIRES)

        inputs = service.get_input_artifacts(run.id)
        assert len(inputs) == 1
        assert inputs[0].artifact_id == inp.id
        assert inputs[0].role == "input"

    def test_finds_inputs_via_manifest(self, service, artifact_repo, dep_repo):
        """Test finding input artifacts via manifest resolved_inputs."""
        inp = _make_artifact(artifact_repo, "s", "input-data")
        run = _make_run("tmpl-1")
        run.metadata["manifest"] = {
            "resolved_inputs": [{"artifact_id": inp.id}],
            "output_artifacts": [],
        }
        service.register_run(run)

        inputs = service.get_input_artifacts(run.id)
        assert len(inputs) == 1
        assert inputs[0].artifact_id == inp.id

    def test_no_duplicates(self, service, artifact_repo, dep_repo):
        """Test inputs are deduplicated across edges and manifest."""
        inp = _make_artifact(artifact_repo, "s", "input-data")
        run = _make_run("tmpl-1")
        run.metadata["manifest"] = {
            "resolved_inputs": [{"artifact_id": inp.id}],
            "output_artifacts": [],
        }
        service.register_run(run)

        _make_edge(dep_repo, inp.id, run.id, run.id, EdgeType.REQUIRES)

        inputs = service.get_input_artifacts(run.id)
        assert len(inputs) == 1


class TestGetOutputArtifacts:
    """Tests for get_output_artifacts."""

    def test_finds_outputs_via_edges(self, service, artifact_repo, dep_repo):
        """Test finding output artifacts via generates edges."""
        output = _make_artifact(artifact_repo, "s", "output")
        run = _make_run("tmpl-1")
        service.register_run(run)

        _make_edge(dep_repo, run.id, output.id, run.id, EdgeType.GENERATES)

        outputs = service.get_output_artifacts(run.id)
        assert len(outputs) == 1
        assert outputs[0].artifact_id == output.id
        assert outputs[0].role == "output"


class TestGetGeneratorRuns:
    """Tests for get_generator_runs."""

    def test_finds_child_runs(self, service):
        """Test finding nested generator runs."""
        parent = _make_run("tmpl-1")
        child1 = _make_run("tmpl-2", parent_run_id=parent.id, depth=1)
        child2 = _make_run("tmpl-3", parent_run_id=parent.id, depth=1)
        unrelated = _make_run("tmpl-4")

        service.register_run(parent)
        service.register_run(child1)
        service.register_run(child2)
        service.register_run(unrelated)

        children = service.get_generator_runs(parent.id)
        child_ids = {c.run_id for c in children}
        assert child1.id in child_ids
        assert child2.id in child_ids
        assert unrelated.id not in child_ids

    def test_no_children(self, service):
        """Test returns empty when no child runs exist."""
        parent = _make_run("tmpl-1")
        service.register_run(parent)
        assert service.get_generator_runs(parent.id) == []


class TestGetValidationHistory:
    """Tests for get_validation_history."""

    def test_returns_empty_without_db(self, artifact_repo, dep_repo):
        """Test returns empty list when no db_path."""
        svc = TraceabilityService(artifact_repo, dep_repo, db_path=None)
        assert svc.get_validation_history("art-1") == []

    def test_returns_results_with_db(self, service):
        """Test returns results from validator when db_path is set."""
        # Without actually writing validation data, should return empty
        results = service.get_validation_history("art-1")
        assert results == []


class TestGetImpactDebt:
    """Tests for get_impact_debt."""

    def test_returns_empty_without_db(self, artifact_repo, dep_repo):
        """Test returns empty list when no db_path."""
        svc = TraceabilityService(artifact_repo, dep_repo, db_path=None)
        assert svc.get_impact_debt("art-1") == []

    def test_returns_empty_no_debt(self, service):
        """Test returns empty when no debt exists."""
        assert service.get_impact_debt("nonexistent") == []


class TestRunSummary:
    """Tests for RunSummary model."""

    def test_create_and_to_dict(self):
        """Test RunSummary creation and serialization."""
        now = datetime.utcnow()
        summary = RunSummary.create(
            run_id="r1",
            template_id="t1",
            status="success",
            stage="complete",
            input_bundle_hash="hash",
            started_at=now,
            completed_at=now,
        )
        d = summary.to_dict()
        assert d["run_id"] == "r1"
        assert d["status"] == "success"
        assert d["completed_at"] is not None

    def test_optional_fields(self):
        """Test RunSummary with optional fields as None."""
        now = datetime.utcnow()
        summary = RunSummary.create(
            run_id="r1",
            template_id="t1",
            status="pending",
            stage="pending",
            input_bundle_hash="hash",
            started_at=now,
        )
        d = summary.to_dict()
        assert d["parent_run_id"] is None
        assert d["completed_at"] is None
        assert d["depth"] == 0


class TestArtifactLineage:
    """Tests for ArtifactLineage model."""

    def test_to_dict(self):
        """Test ArtifactLineage serialization."""
        lineage = ArtifactLineage(
            artifact_id="a1",
            name="test",
            space_id="s1",
            artifact_type="content",
            content_digest="abc123",
            role="input",
        )
        d = lineage.to_dict()
        assert d["artifact_id"] == "a1"
        assert d["role"] == "input"
