"""Tests for the Graph feature — visual skill relationship map."""

import json
import tempfile
from pathlib import Path
from unittest.mock import patch

import pytest

from alive.core.chain import ChainInfo
from alive.core.graph import (
    SkillCluster,
    SkillEdge,
    SkillGraph,
    _build_chain_edges,
    _build_clusters,
    _build_co_creation_edges,
    _build_co_usage_edges,
    _DOMAIN_EMOJI,
    build_graph,
    display_graph,
    export_dot,
)
from alive.core.self_model import RequestRecord, VALID_DOMAINS
from alive.core.skill_registry import SkillInfo, SkillRegistry


# ── Fixtures ────────────────────────────────────────────────────────────────


@pytest.fixture
def sample_chains():
    """Sample chain data."""
    return [
        ChainInfo(
            name="morning-news",
            steps=["web_fetch", "html_parser", "summarizer"],
            description="Chain: web_fetch → html_parser → summarizer",
        ),
        ChainInfo(
            name="backup",
            steps=["file_reader", "cloud_upload"],
            description="Chain: file_reader → cloud_upload",
        ),
    ]


@pytest.fixture
def sample_requests():
    """Sample request records with co-usage data."""
    return [
        RequestRecord(
            id=1,
            request_text="Fetch and parse news",
            timestamp="2026-03-01T10:00:00",
            status="success",
            skills_used=["web_fetch", "html_parser"],
            skills_built=[],
        ),
        RequestRecord(
            id=2,
            request_text="Fetch and summarize",
            timestamp="2026-03-02T10:00:00",
            status="success",
            skills_used=["web_fetch", "summarizer"],
            skills_built=[],
        ),
        RequestRecord(
            id=3,
            request_text="Another fetch and parse",
            timestamp="2026-03-03T10:00:00",
            status="success",
            skills_used=["web_fetch", "html_parser"],
            skills_built=[],
        ),
        RequestRecord(
            id=4,
            request_text="Just password",
            timestamp="2026-03-04T10:00:00",
            status="success",
            skills_used=["password_generator"],
            skills_built=[],
        ),
    ]


@pytest.fixture
def sample_skills():
    """Sample skills with domains."""
    return [
        SkillInfo(name="web_fetch", description="Fetch web pages", domain="web"),
        SkillInfo(name="html_parser", description="Parse HTML content", domain="web"),
        SkillInfo(name="summarizer", description="Summarize text", domain="text"),
        SkillInfo(name="file_reader", description="Read files", domain="file"),
        SkillInfo(name="cloud_upload", description="Upload to cloud", domain="file"),
        SkillInfo(name="password_generator", description="Generate passwords", domain="other"),
        SkillInfo(name="qr_generator", description="Generate QR codes", domain="other"),
    ]


@pytest.fixture
def tmp_skills_dir():
    """Create a temp skills directory with birth stories."""
    with tempfile.TemporaryDirectory() as d:
        skills_dir = Path(d)

        # Create skills with birth stories that share original_request
        for name in ["web_fetch", "html_parser"]:
            skill_dir = skills_dir / name
            skill_dir.mkdir()
            (skill_dir / "birth_story.json").write_text(json.dumps({
                "skill_name": name,
                "original_request": "Build a web scraping pipeline",
                "created_at": "2026-01-15T10:00:00Z",
            }))
            (skill_dir / "metadata.json").write_text(json.dumps({
                "name": name,
                "description": f"Skill {name}",
                "times_used": 0,
                "success_count": 0,
                "fail_count": 0,
                "confidence_score": 0.5,
                "dependencies": [],
                "input_schema": {},
                "output_schema": {},
                "sample_input": {},
            }))
            (skill_dir / "skill.py").write_text(f"def run():\n    return {{}}\n")

        # A lone skill with different request
        lone = skills_dir / "password_generator"
        lone.mkdir()
        (lone / "birth_story.json").write_text(json.dumps({
            "skill_name": "password_generator",
            "original_request": "Generate random passwords",
            "created_at": "2026-01-20T10:00:00Z",
        }))
        (lone / "metadata.json").write_text(json.dumps({
            "name": "password_generator",
            "description": "Generate passwords",
            "times_used": 0,
            "success_count": 0,
            "fail_count": 0,
            "confidence_score": 0.5,
            "dependencies": [],
            "input_schema": {},
            "output_schema": {},
            "sample_input": {},
        }))
        (lone / "skill.py").write_text("def run():\n    return {}\n")

        # A skill without birth story
        no_story = skills_dir / "summarizer"
        no_story.mkdir()
        (no_story / "metadata.json").write_text(json.dumps({
            "name": "summarizer",
            "description": "Summarize text",
            "times_used": 0,
            "success_count": 0,
            "fail_count": 0,
            "confidence_score": 0.5,
            "dependencies": [],
            "input_schema": {},
            "output_schema": {},
            "sample_input": {},
        }))
        (no_story / "skill.py").write_text("def run():\n    return {}\n")

        yield skills_dir


# ── _build_chain_edges tests ───────────────────────────────────────────────


class TestBuildChainEdges:
    def test_basic(self, sample_chains):
        edges = _build_chain_edges(sample_chains)
        assert len(edges) == 3  # 2 from morning-news, 1 from backup

        # morning-news chain
        assert SkillEdge("web_fetch", "html_parser", "chain") in edges
        assert SkillEdge("html_parser", "summarizer", "chain") in edges

        # backup chain
        assert SkillEdge("file_reader", "cloud_upload", "chain") in edges

    def test_empty(self):
        assert _build_chain_edges([]) == []

    def test_two_step_chain(self):
        chains = [ChainInfo(name="simple", steps=["a", "b"])]
        edges = _build_chain_edges(chains)
        assert len(edges) == 1
        assert edges[0].source == "a"
        assert edges[0].target == "b"
        assert edges[0].edge_type == "chain"


# ── _build_co_usage_edges tests ────────────────────────────────────────────


class TestBuildCoUsageEdges:
    def test_basic(self, sample_requests):
        edges = _build_co_usage_edges(sample_requests)

        # web_fetch + html_parser appear together in 2 requests
        wf_hp = [e for e in edges if e.source == "html_parser" and e.target == "web_fetch"]
        assert len(wf_hp) == 1
        assert wf_hp[0].weight == 2

        # web_fetch + summarizer appear together in 1 request
        wf_sum = [e for e in edges if e.source == "summarizer" and e.target == "web_fetch"]
        assert len(wf_sum) == 1
        assert wf_sum[0].weight == 1

    def test_single_skill_no_edges(self):
        requests = [
            RequestRecord(
                id=1, request_text="solo", timestamp="2026-01-01",
                status="success", skills_used=["only_one"], skills_built=[],
            ),
        ]
        assert _build_co_usage_edges(requests) == []

    def test_empty(self):
        assert _build_co_usage_edges([]) == []


# ── _build_co_creation_edges tests ─────────────────────────────────────────


class TestBuildCoCreationEdges:
    def test_shared_request(self, tmp_skills_dir):
        edges = _build_co_creation_edges(skills_dir=tmp_skills_dir)
        # web_fetch and html_parser share "Build a web scraping pipeline"
        assert len(edges) == 1
        assert edges[0].source == "html_parser"
        assert edges[0].target == "web_fetch"
        assert edges[0].edge_type == "co_creation"

    def test_no_stories(self):
        with tempfile.TemporaryDirectory() as d:
            assert _build_co_creation_edges(skills_dir=Path(d)) == []

    def test_nonexistent_dir(self):
        assert _build_co_creation_edges(skills_dir=Path("/nonexistent")) == []


# ── _build_clusters tests ──────────────────────────────────────────────────


class TestBuildClusters:
    def test_by_domain(self, sample_skills):
        edges = [SkillEdge("web_fetch", "html_parser", "chain")]
        clusters = _build_clusters(sample_skills, edges)

        names = [c.name for c in clusters]
        # "other" skills with no edges → "Standalone"
        assert "Standalone" in names
        assert "Web cluster" in names
        assert "File cluster" in names
        assert "Text cluster" in names

    def test_standalone_only_for_unconnected_other(self, sample_skills):
        # No edges at all — "other" domain skills should go to Standalone
        clusters = _build_clusters(sample_skills, [])

        standalone = next(c for c in clusters if c.name == "Standalone")
        assert "password_generator" in standalone.skills
        assert "qr_generator" in standalone.skills

    def test_connected_other_not_standalone(self, sample_skills):
        # If an "other" skill has edges, it should NOT be in Standalone
        edges = [SkillEdge("password_generator", "qr_generator", "co_usage")]
        clusters = _build_clusters(sample_skills, edges)

        other_cluster = next(
            (c for c in clusters if "Other" in c.name), None
        )
        # Both should be in "Other cluster", not Standalone
        assert other_cluster is not None
        assert "password_generator" in other_cluster.skills

    def test_empty_skills(self):
        assert _build_clusters([], []) == []


# ── build_graph integration tests ──────────────────────────────────────────


class TestBuildGraph:
    @pytest.mark.asyncio
    async def test_integration(self, tmp_skills_dir):
        registry = SkillRegistry(skills_dir=tmp_skills_dir)

        with patch("alive.core.graph.list_chains", return_value=[]):
            with patch("alive.core.graph.list_requests", return_value=[]):
                graph = await build_graph(
                    registry=registry,
                    skills_dir=tmp_skills_dir,
                )

        assert isinstance(graph, SkillGraph)
        assert len(graph.skills) > 0
        assert isinstance(graph.clusters, list)

    @pytest.mark.asyncio
    async def test_empty_registry(self):
        with tempfile.TemporaryDirectory() as d:
            empty_dir = Path(d) / "skills"
            empty_dir.mkdir()
            registry = SkillRegistry(skills_dir=empty_dir)

            with patch("alive.core.graph.list_chains", return_value=[]):
                with patch("alive.core.graph.list_requests", return_value=[]):
                    graph = await build_graph(
                        registry=registry,
                        skills_dir=empty_dir,
                    )

            # May have seed skills, but graph should at least not crash
            assert isinstance(graph, SkillGraph)

    @pytest.mark.asyncio
    async def test_with_chains(self, tmp_skills_dir):
        registry = SkillRegistry(skills_dir=tmp_skills_dir)

        chains = [
            ChainInfo(
                name="test-chain",
                steps=["web_fetch", "summarizer"],
            ),
        ]

        with patch("alive.core.graph.list_chains", return_value=chains):
            with patch("alive.core.graph.list_requests", return_value=[]):
                graph = await build_graph(
                    registry=registry,
                    skills_dir=tmp_skills_dir,
                )

        assert len(graph.chains) == 1
        chain_edges = [e for e in graph.edges if e.edge_type == "chain"]
        assert len(chain_edges) == 1


# ── display_graph tests ────────────────────────────────────────────────────


class TestDisplayGraph:
    def test_basic(self, sample_skills, sample_chains):
        from rich.console import Console

        graph = SkillGraph(
            skills=sample_skills,
            edges=_build_chain_edges(sample_chains),
            clusters=_build_clusters(
                sample_skills,
                _build_chain_edges(sample_chains),
            ),
            chains=sample_chains,
        )
        console = Console(file=open("/dev/null", "w"), force_terminal=False)
        display_graph(graph, console=console)

    def test_empty_graph(self):
        from rich.console import Console

        graph = SkillGraph()
        console = Console(file=open("/dev/null", "w"), force_terminal=False)
        display_graph(graph, console=console)

    def test_no_chains(self, sample_skills):
        from rich.console import Console

        graph = SkillGraph(
            skills=sample_skills,
            edges=[],
            clusters=_build_clusters(sample_skills, []),
            chains=[],
        )
        console = Console(file=open("/dev/null", "w"), force_terminal=False)
        display_graph(graph, console=console)


# ── export_dot tests ───────────────────────────────────────────────────────


class TestExportDot:
    def test_basic_structure(self, sample_skills, sample_chains):
        graph = SkillGraph(
            skills=sample_skills,
            edges=_build_chain_edges(sample_chains),
            clusters=_build_clusters(
                sample_skills,
                _build_chain_edges(sample_chains),
            ),
            chains=sample_chains,
        )
        dot = export_dot(graph)
        assert "digraph skills" in dot
        assert "rankdir=LR" in dot

    def test_nodes_present(self, sample_skills):
        graph = SkillGraph(
            skills=sample_skills,
            edges=[],
            clusters=_build_clusters(sample_skills, []),
            chains=[],
        )
        dot = export_dot(graph)
        assert "web_fetch" in dot
        assert "html_parser" in dot
        assert "summarizer" in dot

    def test_chain_edges(self, sample_skills, sample_chains):
        edges = _build_chain_edges(sample_chains)
        graph = SkillGraph(
            skills=sample_skills,
            edges=edges,
            clusters=_build_clusters(sample_skills, edges),
            chains=sample_chains,
        )
        dot = export_dot(graph)
        assert "web_fetch -> html_parser" in dot
        assert "style=solid" in dot

    def test_co_usage_edges(self, sample_skills):
        edges = [SkillEdge("web_fetch", "html_parser", "co_usage", weight=3)]
        graph = SkillGraph(
            skills=sample_skills,
            edges=edges,
            clusters=_build_clusters(sample_skills, edges),
            chains=[],
        )
        dot = export_dot(graph)
        assert "style=dashed" in dot
        assert '3x' in dot

    def test_empty_graph(self):
        graph = SkillGraph()
        dot = export_dot(graph)
        assert "digraph skills" in dot
        assert "}" in dot

    def test_chain_subgraphs(self, sample_skills, sample_chains):
        graph = SkillGraph(
            skills=sample_skills,
            edges=_build_chain_edges(sample_chains),
            clusters=_build_clusters(
                sample_skills,
                _build_chain_edges(sample_chains),
            ),
            chains=sample_chains,
        )
        dot = export_dot(graph)
        assert "subgraph cluster_morning_news" in dot
        assert "subgraph cluster_backup" in dot


# ── Dataclass tests ────────────────────────────────────────────────────────


class TestDataclasses:
    def test_skill_edge_defaults(self):
        e = SkillEdge(source="a", target="b", edge_type="chain")
        assert e.weight == 1

    def test_skill_edge_with_weight(self):
        e = SkillEdge(source="a", target="b", edge_type="co_usage", weight=5)
        assert e.weight == 5

    def test_skill_cluster(self):
        c = SkillCluster(name="Web cluster", emoji="🌐", skills=["a", "b"])
        assert c.name == "Web cluster"
        assert len(c.skills) == 2

    def test_skill_cluster_defaults(self):
        c = SkillCluster(name="Empty", emoji="⚡")
        assert c.skills == []

    def test_skill_graph_defaults(self):
        g = SkillGraph()
        assert g.skills == []
        assert g.edges == []
        assert g.clusters == []
        assert g.chains == []


# ── Domain emoji mapping test ──────────────────────────────────────────────


class TestDomainEmoji:
    def test_all_domains_have_emoji(self):
        for domain in VALID_DOMAINS:
            assert domain in _DOMAIN_EMOJI, f"Missing emoji for domain: {domain}"
