"""Tests for the Skill Chaining feature."""

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

import pytest

from alive.core.chain import (
    ChainInfo,
    ChainResult,
    ChainStepResult,
    create_chain,
    delete_chain,
    describe_cron,
    display_chain_created,
    display_chains,
    is_chain,
    list_chains,
    load_chain,
    run_chain,
)
from alive.core.skill_registry import SkillInfo, SkillRegistry


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


@pytest.fixture
def tmp_skills_dir():
    """Create a temporary skills directory with 3 fake skills."""
    with tempfile.TemporaryDirectory() as d:
        skills_dir = Path(d)

        # Create web_fetch skill
        wf_dir = skills_dir / "web_fetch"
        wf_dir.mkdir()
        (wf_dir / "skill.py").write_text(
            'def run(url: str = "https://example.com") -> dict:\n'
            '    return {"content": "Hello", "status": 200}\n'
        )
        (wf_dir / "metadata.json").write_text(json.dumps({
            "name": "web_fetch",
            "description": "Fetch content from a URL",
            "times_used": 3,
            "success_count": 3,
            "fail_count": 0,
            "confidence_score": 0.8,
            "dependencies": [],
            "input_schema": {"url": "str"},
            "output_schema": {"content": "str", "status": "int"},
            "sample_input": {"url": "https://example.com"},
        }))

        # Create html_parser skill
        hp_dir = skills_dir / "html_parser"
        hp_dir.mkdir()
        (hp_dir / "skill.py").write_text(
            'def run(content: str = "", status: int = 200) -> dict:\n'
            '    return {"text": content.upper(), "links": []}\n'
        )
        (hp_dir / "metadata.json").write_text(json.dumps({
            "name": "html_parser",
            "description": "Parse HTML content",
            "times_used": 2,
            "success_count": 2,
            "fail_count": 0,
            "confidence_score": 0.7,
            "dependencies": [],
            "input_schema": {"content": "str"},
            "output_schema": {"text": "str", "links": "list"},
            "sample_input": {"content": "<p>Hello</p>"},
        }))

        # Create summarizer skill
        s_dir = skills_dir / "summarizer"
        s_dir.mkdir()
        (s_dir / "skill.py").write_text(
            'def run(text: str = "", links: list = None) -> dict:\n'
            '    return {"summary": text[:50], "success": True}\n'
        )
        (s_dir / "metadata.json").write_text(json.dumps({
            "name": "summarizer",
            "description": "Summarize text",
            "times_used": 1,
            "success_count": 1,
            "fail_count": 0,
            "confidence_score": 0.6,
            "dependencies": [],
            "input_schema": {"text": "str"},
            "output_schema": {"summary": "str", "success": "bool"},
            "sample_input": {"text": "Some long text to summarize."},
        }))

        yield skills_dir


@pytest.fixture
def registry(tmp_skills_dir):
    """Create a SkillRegistry with the tmp_skills_dir."""
    return SkillRegistry(skills_dir=tmp_skills_dir)


# ── create_chain tests ──────────────────────────────────────────────────────


class TestCreateChain:
    def test_basic_creation(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            chain = create_chain(
                "my-chain",
                ["web_fetch", "html_parser"],
                skills_dir=tmp_skills_dir,
            )

        assert chain.name == "my-chain"
        assert chain.steps == ["web_fetch", "html_parser"]
        assert chain.description == "Chain: web_fetch → html_parser"
        assert chain.created_at != ""

        # Verify files on disk
        chain_dir = tmp_skills_dir / "my-chain"
        assert (chain_dir / "chain.json").exists()
        assert (chain_dir / "metadata.json").exists()

        # Verify chain.json contents
        data = json.loads((chain_dir / "chain.json").read_text())
        assert data["name"] == "my-chain"
        assert data["steps"] == ["web_fetch", "html_parser"]

        # Verify metadata.json has chain type
        meta = json.loads((chain_dir / "metadata.json").read_text())
        assert meta["skill_type"] == "chain"

    def test_validates_skills_exist(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            with pytest.raises(ValueError, match="not found"):
                create_chain(
                    "bad-chain",
                    ["web_fetch", "nonexistent_skill"],
                    skills_dir=tmp_skills_dir,
                )

    def test_minimum_two_steps(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            with pytest.raises(ValueError, match="at least 2"):
                create_chain("solo", ["web_fetch"], skills_dir=tmp_skills_dir)

    def test_duplicate_name_raises(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            create_chain(
                "dup-chain",
                ["web_fetch", "html_parser"],
                skills_dir=tmp_skills_dir,
            )
            with pytest.raises(FileExistsError, match="already exists"):
                create_chain(
                    "dup-chain",
                    ["web_fetch", "summarizer"],
                    skills_dir=tmp_skills_dir,
                )

    def test_default_input_from_sample(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            chain = create_chain(
                "auto-input",
                ["web_fetch", "html_parser"],
                skills_dir=tmp_skills_dir,
            )

        # Should use web_fetch's sample_input
        assert chain.default_input == {"url": "https://example.com"}

    def test_custom_default_input(self, tmp_skills_dir, registry):
        custom_input = {"url": "https://news.ycombinator.com"}
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            chain = create_chain(
                "custom-input",
                ["web_fetch", "html_parser"],
                default_input=custom_input,
                skills_dir=tmp_skills_dir,
            )

        assert chain.default_input == custom_input

    def test_with_schedule(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            chain = create_chain(
                "scheduled",
                ["web_fetch", "html_parser"],
                schedule="0 8 * * *",
                skills_dir=tmp_skills_dir,
            )

        assert chain.schedule == "0 8 * * *"


# ── load_chain tests ────────────────────────────────────────────────────────


class TestLoadChain:
    def test_load_existing(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            create_chain(
                "load-test",
                ["web_fetch", "html_parser", "summarizer"],
                skills_dir=tmp_skills_dir,
            )

        loaded = load_chain("load-test", skills_dir=tmp_skills_dir)
        assert loaded.name == "load-test"
        assert loaded.steps == ["web_fetch", "html_parser", "summarizer"]
        assert loaded.description == "Chain: web_fetch → html_parser → summarizer"

    def test_load_not_found(self, tmp_skills_dir):
        with pytest.raises(FileNotFoundError, match="No chain named"):
            load_chain("ghost-chain", skills_dir=tmp_skills_dir)


# ── list_chains tests ───────────────────────────────────────────────────────


class TestListChains:
    def test_empty(self, tmp_skills_dir):
        result = list_chains(skills_dir=tmp_skills_dir)
        assert result == []

    def test_returns_only_chains(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            create_chain(
                "my-chain",
                ["web_fetch", "html_parser"],
                skills_dir=tmp_skills_dir,
            )

        chains = list_chains(skills_dir=tmp_skills_dir)
        assert len(chains) == 1
        assert chains[0].name == "my-chain"

        # Regular skills should not appear
        names = [c.name for c in chains]
        assert "web_fetch" not in names
        assert "html_parser" not in names


# ── delete_chain tests ──────────────────────────────────────────────────────


class TestDeleteChain:
    def test_delete_existing(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            create_chain(
                "doomed",
                ["web_fetch", "html_parser"],
                skills_dir=tmp_skills_dir,
            )

        assert (tmp_skills_dir / "doomed").exists()
        delete_chain("doomed", skills_dir=tmp_skills_dir)
        assert not (tmp_skills_dir / "doomed").exists()

        # list_chains should be empty now
        assert list_chains(skills_dir=tmp_skills_dir) == []

    def test_delete_not_found(self, tmp_skills_dir):
        with pytest.raises(FileNotFoundError, match="No chain named"):
            delete_chain("nonexistent", skills_dir=tmp_skills_dir)


# ── is_chain tests ──────────────────────────────────────────────────────────


class TestIsChain:
    def test_true_for_chain(self, tmp_skills_dir, registry):
        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            create_chain(
                "check-me",
                ["web_fetch", "html_parser"],
                skills_dir=tmp_skills_dir,
            )

        assert is_chain("check-me", skills_dir=tmp_skills_dir) is True

    def test_false_for_regular_skill(self, tmp_skills_dir):
        assert is_chain("web_fetch", skills_dir=tmp_skills_dir) is False

    def test_false_for_nonexistent(self, tmp_skills_dir):
        assert is_chain("nope", skills_dir=tmp_skills_dir) is False


# ── run_chain tests ─────────────────────────────────────────────────────────


class TestRunChain:
    def test_success_pipes_output(self, tmp_skills_dir, registry):
        """Verify that step N output is passed as step N+1 input."""
        from alive.sandbox.runner import SkillResult

        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            create_chain(
                "pipe-test",
                ["web_fetch", "html_parser"],
                default_input={"url": "https://example.com"},
                skills_dir=tmp_skills_dir,
            )

        # Track what inputs each step receives
        call_inputs = []

        def mock_run_skill(name, input_data, **kwargs):
            call_inputs.append((name, input_data))
            if name == "web_fetch":
                return SkillResult(success=True, output={"content": "Hello", "status": 200}, error=None, duration_ms=100)
            elif name == "html_parser":
                return SkillResult(success=True, output={"text": "HELLO", "links": []}, error=None, duration_ms=50)
            return SkillResult(success=False, output=None, error="unknown skill", duration_ms=0)

        from rich.console import Console

        with patch("alive.sandbox.runner.run_skill", side_effect=mock_run_skill):
            with patch("alive.core.skill_registry.get_registry", return_value=registry):
                result = run_chain(
                    "pipe-test",
                    skills_dir=tmp_skills_dir,
                    console=Console(quiet=True),
                )

        assert result.success is True
        assert result.final_output == {"text": "HELLO", "links": []}
        assert len(result.step_results) == 2

        # Verify piping: html_parser received web_fetch's output
        assert call_inputs[0] == ("web_fetch", {"url": "https://example.com"})
        assert call_inputs[1] == ("html_parser", {"content": "Hello", "status": 200})

    def test_step_failure_stops_chain(self, tmp_skills_dir, registry):
        from alive.sandbox.runner import SkillResult

        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            create_chain(
                "fail-test",
                ["web_fetch", "html_parser"],
                default_input={"url": "https://example.com"},
                skills_dir=tmp_skills_dir,
            )

        def mock_run_skill(name, input_data, **kwargs):
            if name == "web_fetch":
                return SkillResult(success=False, output=None, error="Connection refused", duration_ms=50)
            return SkillResult(success=True, output={}, error=None, duration_ms=10)

        from rich.console import Console

        with patch("alive.sandbox.runner.run_skill", side_effect=mock_run_skill):
            with patch("alive.core.skill_registry.get_registry", return_value=registry):
                result = run_chain(
                    "fail-test",
                    skills_dir=tmp_skills_dir,
                    console=Console(quiet=True),
                )

        assert result.success is False
        assert len(result.step_results) == 1  # stopped at first step
        assert result.step_results[0].error == "Connection refused"

    def test_input_override(self, tmp_skills_dir, registry):
        from alive.sandbox.runner import SkillResult

        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            create_chain(
                "override-test",
                ["web_fetch", "html_parser"],
                default_input={"url": "https://default.com"},
                skills_dir=tmp_skills_dir,
            )

        call_inputs = []

        def mock_run_skill(name, input_data, **kwargs):
            call_inputs.append((name, input_data))
            return SkillResult(success=True, output={"content": "x", "status": 200}, error=None, duration_ms=10)

        from rich.console import Console

        with patch("alive.sandbox.runner.run_skill", side_effect=mock_run_skill):
            with patch("alive.core.skill_registry.get_registry", return_value=registry):
                run_chain(
                    "override-test",
                    input_override={"url": "https://override.com"},
                    skills_dir=tmp_skills_dir,
                    console=Console(quiet=True),
                )

        # First step should use the override, not the default
        assert call_inputs[0][1] == {"url": "https://override.com"}

    def test_uses_default_input(self, tmp_skills_dir, registry):
        from alive.sandbox.runner import SkillResult

        with patch("alive.core.skill_registry.get_registry", return_value=registry):
            create_chain(
                "default-test",
                ["web_fetch", "html_parser"],
                default_input={"url": "https://default.com"},
                skills_dir=tmp_skills_dir,
            )

        call_inputs = []

        def mock_run_skill(name, input_data, **kwargs):
            call_inputs.append((name, input_data))
            return SkillResult(success=True, output={"content": "x"}, error=None, duration_ms=10)

        from rich.console import Console

        with patch("alive.sandbox.runner.run_skill", side_effect=mock_run_skill):
            with patch("alive.core.skill_registry.get_registry", return_value=registry):
                run_chain(
                    "default-test",
                    skills_dir=tmp_skills_dir,
                    console=Console(quiet=True),
                )

        # Should use default_input
        assert call_inputs[0][1] == {"url": "https://default.com"}


# ── Registry integration tests ──────────────────────────────────────────────


class TestRegistryIntegration:
    def test_chain_appears_in_list_all(self, tmp_skills_dir):
        """After creating a chain, a fresh registry should include it."""
        reg = SkillRegistry(skills_dir=tmp_skills_dir)

        with patch("alive.core.skill_registry.get_registry", return_value=reg):
            create_chain(
                "reg-test",
                ["web_fetch", "html_parser"],
                skills_dir=tmp_skills_dir,
            )

        # Reload registry
        fresh_reg = SkillRegistry(skills_dir=tmp_skills_dir)
        all_names = [s.name for s in fresh_reg.list_all()]
        assert "reg-test" in all_names

        # Check skill_type
        chain_info = fresh_reg.get_skill("reg-test")
        assert chain_info.skill_type == "chain"

    def test_chain_excluded_from_skills_only(self, tmp_skills_dir):
        reg = SkillRegistry(skills_dir=tmp_skills_dir)

        with patch("alive.core.skill_registry.get_registry", return_value=reg):
            create_chain(
                "excluded-test",
                ["web_fetch", "html_parser"],
                skills_dir=tmp_skills_dir,
            )

        fresh_reg = SkillRegistry(skills_dir=tmp_skills_dir)
        skills_only = fresh_reg.list_skills_only()
        names = [s.name for s in skills_only]
        assert "excluded-test" not in names
        # Regular skills should still be there
        assert "web_fetch" in names

    def test_delete_removes_from_registry(self, tmp_skills_dir):
        reg = SkillRegistry(skills_dir=tmp_skills_dir)

        with patch("alive.core.skill_registry.get_registry", return_value=reg):
            create_chain(
                "del-reg-test",
                ["web_fetch", "html_parser"],
                skills_dir=tmp_skills_dir,
            )

        delete_chain("del-reg-test", skills_dir=tmp_skills_dir)

        # Fresh registry should not include it
        fresh_reg = SkillRegistry(skills_dir=tmp_skills_dir)
        all_names = [s.name for s in fresh_reg.list_all()]
        assert "del-reg-test" not in all_names


# ── describe_cron tests ─────────────────────────────────────────────────────


class TestDescribeCron:
    def test_daily(self):
        result = describe_cron("0 8 * * *")
        assert "daily" in result
        assert "08:00" in result

    def test_weekdays(self):
        result = describe_cron("0 9 * * 1-5")
        assert "weekdays" in result
        assert "09:00" in result

    def test_weekends(self):
        result = describe_cron("30 10 * * 0,6")
        assert "weekends" in result
        assert "10:30" in result

    def test_specific_day(self):
        result = describe_cron("0 7 * * 1")
        assert "Monday" in result
        assert "07:00" in result

    def test_monthly(self):
        result = describe_cron("0 0 1 * *")
        assert "month" in result

    def test_invalid_format(self):
        result = describe_cron("not-a-cron")
        assert "schedule:" in result

    def test_complex_fallback(self):
        result = describe_cron("*/5 * * * *")
        # Every 5 minutes — should produce some output without crashing
        assert result != ""


# ── Display tests ───────────────────────────────────────────────────────────


class TestDisplay:
    def test_display_chain_created(self):
        from rich.console import Console

        chain = ChainInfo(
            name="test-chain",
            steps=["web_fetch", "html_parser", "summarizer"],
            description="Chain: web_fetch → html_parser → summarizer",
        )
        console = Console(file=open("/dev/null", "w"), force_terminal=False)
        # Should not crash
        display_chain_created(chain, schedule_desc="daily at 08:00", console=console)

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

        chain = ChainInfo(name="simple", steps=["a", "b"])
        console = Console(file=open("/dev/null", "w"), force_terminal=False)
        display_chain_created(chain, console=console)

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

        console = Console(file=open("/dev/null", "w"), force_terminal=False)
        display_chains([], console=console)

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

        chains = [
            ChainInfo(
                name="morning-news",
                steps=["web_fetch", "summarizer"],
                schedule="0 8 * * *",
                times_run=5,
                last_run="2024-01-15T08:00:00Z",
            ),
            ChainInfo(
                name="backup",
                steps=["file_reader", "cloud_upload"],
                times_run=0,
            ),
        ]
        console = Console(file=open("/dev/null", "w"), force_terminal=False)
        display_chains(chains, console=console)


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


class TestDataclasses:
    def test_chain_info_defaults(self):
        c = ChainInfo(name="test", steps=["a", "b"])
        assert c.default_input == {}
        assert c.schedule is None
        assert c.times_run == 0
        assert c.last_run == ""

    def test_chain_step_result(self):
        r = ChainStepResult(skill_name="web_fetch", success=True, output={"x": 1}, duration_ms=100)
        assert r.skill_name == "web_fetch"
        assert r.success is True
        assert r.error is None

    def test_chain_result(self):
        r = ChainResult(
            chain_name="test",
            step_results=[
                ChainStepResult(skill_name="a", success=True, duration_ms=50),
                ChainStepResult(skill_name="b", success=True, duration_ms=60),
            ],
            final_output={"result": "done"},
            success=True,
            total_duration_ms=110,
        )
        assert r.success is True
        assert len(r.step_results) == 2
        assert r.total_duration_ms == 110
