"""Tests for ievo.core.agent_sync — mtime-based marketplace synchronisation."""

from __future__ import annotations

from pathlib import Path

import yaml

from ievo.core.agent_sync import sync_marketplace
from ievo.core.config import IEVO_REGISTRY
from ievo.core.templates import TEMPLATE_MAP
from ievo.marketplace import (
    bundled_agent_path,
    bundled_skill_path,
    bundled_template_path,
    list_bundled_agents,
    list_bundled_skills,
)

# ── First run (no registry) ──────────────────────────────────


def test_sync_first_run_deploys_all_agents(tmp_path: Path) -> None:
    """First sync deploys all bundled agents as .md files."""
    updated = sync_marketplace(tmp_path)

    agents_dir = tmp_path / ".claude" / "agents"
    for name in list_bundled_agents():
        assert (agents_dir / f"{name}.md").exists()
        assert name in updated


def test_sync_first_run_deploys_all_skills(tmp_path: Path) -> None:
    """First sync deploys all bundled skills."""
    sync_marketplace(tmp_path)

    skills_dir = tmp_path / ".claude" / "skills"
    for name in list_bundled_skills():
        assert (skills_dir / name / "SKILL.md").exists()


def test_sync_first_run_deploys_all_templates(tmp_path: Path) -> None:
    """First sync deploys all bundled templates to their project destinations."""
    sync_marketplace(tmp_path)

    for _, dest_rel in TEMPLATE_MAP:
        assert (tmp_path / dest_rel).exists(), f"Missing: {dest_rel}"


def test_sync_first_run_creates_registry(tmp_path: Path) -> None:
    """First sync creates .ievo/registry.yaml."""
    sync_marketplace(tmp_path)

    assert (tmp_path / IEVO_REGISTRY).exists()


def test_sync_registry_structure(tmp_path: Path) -> None:
    """Registry yaml has agents, skills, templates sections with float mtimes."""
    sync_marketplace(tmp_path)

    data = yaml.safe_load((tmp_path / IEVO_REGISTRY).read_text())

    assert "agents" in data
    assert "skills" in data
    assert "templates" in data

    for name in list_bundled_agents():
        assert name in data["agents"]
        assert isinstance(data["agents"][name], float)

    for name in list_bundled_skills():
        assert name in data["skills"]
        assert isinstance(data["skills"][name], float)

    for bundled_name, _ in TEMPLATE_MAP:
        stem = Path(bundled_name).stem
        assert stem in data["templates"]
        assert isinstance(data["templates"][stem], float)


# ── Second run (no changes) ──────────────────────────────────


def test_sync_second_run_returns_empty(tmp_path: Path) -> None:
    """Second sync with unchanged bundled files returns empty list."""
    sync_marketplace(tmp_path)
    updated = sync_marketplace(tmp_path)

    assert updated == []


def test_sync_second_run_does_not_overwrite_agent(tmp_path: Path) -> None:
    """Second sync does not re-copy an agent whose bundled mtime is unchanged."""
    sync_marketplace(tmp_path)

    name = list_bundled_agents()[0]
    deployed = tmp_path / ".claude" / "agents" / f"{name}.md"
    deployed.write_text("# custom content\n")

    sync_marketplace(tmp_path)

    assert deployed.read_text() == "# custom content\n"


def test_sync_redeploys_deleted_agent(tmp_path: Path) -> None:
    """Deleted deployed agent is redeployed even if bundled mtime is unchanged."""
    sync_marketplace(tmp_path)

    name = list_bundled_agents()[0]
    deployed = tmp_path / ".claude" / "agents" / f"{name}.md"
    deployed.unlink()

    updated = sync_marketplace(tmp_path)

    assert name in updated
    assert deployed.exists()


def test_sync_redeploys_deleted_skill(tmp_path: Path) -> None:
    """Deleted deployed skill is redeployed even if bundled mtime is unchanged."""
    sync_marketplace(tmp_path)

    name = list_bundled_skills()[0]
    deployed = tmp_path / ".claude" / "skills" / name / "SKILL.md"
    deployed.unlink()

    updated = sync_marketplace(tmp_path)

    assert name in updated
    assert deployed.exists()


def test_sync_redeploys_deleted_template(tmp_path: Path) -> None:
    """Deleted deployed template is redeployed even if bundled mtime is unchanged."""
    sync_marketplace(tmp_path)

    bundled_name, dest_rel = TEMPLATE_MAP[0]
    deployed = tmp_path / dest_rel
    deployed.unlink()

    updated = sync_marketplace(tmp_path)

    assert bundled_name in updated
    assert deployed.exists()


def test_sync_second_run_does_not_overwrite_skill(tmp_path: Path) -> None:
    """Second sync does not re-copy a skill whose bundled mtime is unchanged."""
    sync_marketplace(tmp_path)

    name = list_bundled_skills()[0]
    deployed = tmp_path / ".claude" / "skills" / name / "SKILL.md"
    deployed.write_text("# custom skill\n")

    sync_marketplace(tmp_path)

    assert deployed.read_text() == "# custom skill\n"


# ── Changed mtime triggers redeploy ──────────────────────────


def test_sync_changed_mtime_redeploys_agent(tmp_path: Path) -> None:
    """Agent with stale stored mtime is redeployed from bundled source."""
    sync_marketplace(tmp_path)

    registry_path = tmp_path / IEVO_REGISTRY
    data = yaml.safe_load(registry_path.read_text())
    name = list_bundled_agents()[0]
    data["agents"][name] = 0.0  # Fake old mtime
    registry_path.write_text(yaml.dump(data))

    deployed = tmp_path / ".claude" / "agents" / f"{name}.md"
    deployed.write_text("# outdated content\n")

    updated = sync_marketplace(tmp_path)

    assert name in updated
    bundled = bundled_agent_path(name)
    assert bundled is not None
    assert deployed.read_text() == bundled.read_text()


def test_sync_changed_mtime_redeploys_skill(tmp_path: Path) -> None:
    """Skill with stale stored mtime is redeployed from bundled source."""
    sync_marketplace(tmp_path)

    registry_path = tmp_path / IEVO_REGISTRY
    data = yaml.safe_load(registry_path.read_text())
    name = list_bundled_skills()[0]
    data["skills"][name] = 0.0  # Fake old mtime
    registry_path.write_text(yaml.dump(data))

    deployed = tmp_path / ".claude" / "skills" / name / "SKILL.md"
    deployed.write_text("# outdated skill\n")

    updated = sync_marketplace(tmp_path)

    assert name in updated
    bundled = bundled_skill_path(name)
    assert bundled is not None
    assert deployed.read_text() == bundled.read_text()


def test_sync_changed_mtime_redeploys_template(tmp_path: Path) -> None:
    """Template with stale stored mtime is redeployed from bundled source."""
    sync_marketplace(tmp_path)

    registry_path = tmp_path / IEVO_REGISTRY
    data = yaml.safe_load(registry_path.read_text())
    bundled_name, dest_rel = TEMPLATE_MAP[0]
    stem = Path(bundled_name).stem
    data["templates"][stem] = 0.0  # Fake old mtime
    registry_path.write_text(yaml.dump(data))

    deployed = tmp_path / dest_rel
    deployed.write_text("# outdated template\n")

    updated = sync_marketplace(tmp_path)

    assert bundled_name in updated
    src = bundled_template_path(bundled_name)
    assert src is not None
    assert deployed.read_text() == src.read_text()


def test_sync_updates_registry_after_redeploy(tmp_path: Path) -> None:
    """After redeploying, registry stores the real (current) mtime."""
    sync_marketplace(tmp_path)

    registry_path = tmp_path / IEVO_REGISTRY
    data = yaml.safe_load(registry_path.read_text())
    name = list_bundled_agents()[0]
    original_mtime = data["agents"][name]

    data["agents"][name] = 0.0
    registry_path.write_text(yaml.dump(data))

    sync_marketplace(tmp_path)

    updated_data = yaml.safe_load(registry_path.read_text())
    assert updated_data["agents"][name] == original_mtime


# ── Edge cases ──────────────────────────────────────────────


def test_sync_invalid_yaml_registry_deploys_all(tmp_path: Path) -> None:
    """Corrupted registry.yaml is silently ignored — everything is deployed."""
    registry_path = tmp_path / IEVO_REGISTRY
    registry_path.parent.mkdir(parents=True, exist_ok=True)
    registry_path.write_text(": invalid: [\n")

    updated = sync_marketplace(tmp_path)

    assert len(updated) > 0


def test_sync_empty_registry_deploys_all(tmp_path: Path) -> None:
    """Empty registry.yaml (safe_load → None) is treated as first run."""
    registry_path = tmp_path / IEVO_REGISTRY
    registry_path.parent.mkdir(parents=True, exist_ok=True)
    registry_path.write_text("")

    updated = sync_marketplace(tmp_path)

    assert len(updated) > 0


def test_sync_creates_target_directories(tmp_path: Path) -> None:
    """sync_marketplace creates .claude/agents/ and .claude/skills/ even when absent."""
    assert not (tmp_path / ".claude").exists()

    sync_marketplace(tmp_path)

    assert (tmp_path / ".claude" / "agents").is_dir()
    assert (tmp_path / ".claude" / "skills").is_dir()


def test_sync_returns_sorted_list(tmp_path: Path) -> None:
    """Returned updated list is always sorted."""
    updated = sync_marketplace(tmp_path)

    assert updated == sorted(updated)


def test_sync_agent_content_matches_bundled(tmp_path: Path) -> None:
    """Deployed agent content is byte-identical to bundled source."""
    sync_marketplace(tmp_path)

    for name in list_bundled_agents():
        deployed = (tmp_path / ".claude" / "agents" / f"{name}.md").read_text()
        bundled = bundled_agent_path(name)
        assert bundled is not None
        assert deployed == bundled.read_text()


def test_sync_skill_content_matches_bundled(tmp_path: Path) -> None:
    """Deployed skill content is byte-identical to bundled source."""
    sync_marketplace(tmp_path)

    for name in list_bundled_skills():
        deployed = (tmp_path / ".claude" / "skills" / name / "SKILL.md").read_text()
        bundled = bundled_skill_path(name)
        assert bundled is not None
        assert deployed == bundled.read_text()


def test_sync_template_content_matches_bundled(tmp_path: Path) -> None:
    """Deployed template content is byte-identical to bundled source."""
    sync_marketplace(tmp_path)

    for bundled_name, dest_rel in TEMPLATE_MAP:
        deployed = (tmp_path / dest_rel).read_text()
        src = bundled_template_path(bundled_name)
        assert src is not None
        assert deployed == src.read_text()


def test_sync_skips_missing_bundled_agent(tmp_path: Path) -> None:
    """Agents with no bundled source (bundled_agent_path returns None) are skipped."""
    from unittest.mock import patch

    with patch("ievo.core.agent_sync.bundled_agent_path", return_value=None):
        updated = sync_marketplace(tmp_path)

    assert not any((tmp_path / ".claude" / "agents").glob("*.md"))
    assert all(name not in updated for name in list_bundled_agents())


def test_sync_skips_missing_bundled_skill(tmp_path: Path) -> None:
    """Skills with no bundled source (bundled_skill_path returns None) are skipped."""
    from unittest.mock import patch

    with patch("ievo.core.agent_sync.bundled_skill_path", return_value=None):
        updated = sync_marketplace(tmp_path)

    assert all(name not in updated for name in list_bundled_skills())


def test_sync_skips_missing_bundled_template(tmp_path: Path) -> None:
    """Templates with no bundled source (bundled_template_path returns None) are skipped."""
    from unittest.mock import patch

    with patch("ievo.core.agent_sync.bundled_template_path", return_value=None):
        sync_marketplace(tmp_path)

    for _, dest_rel in TEMPLATE_MAP:
        assert not (tmp_path / dest_rel).exists()


def test_sync_partial_registry_redeploys_missing(tmp_path: Path) -> None:
    """Agents absent from stored registry are deployed; recorded agents are skipped."""
    # Create a registry that only records the first agent with current mtime
    registry_path = tmp_path / IEVO_REGISTRY
    registry_path.parent.mkdir(parents=True, exist_ok=True)
    agents_dir = tmp_path / ".claude" / "agents"
    agents_dir.mkdir(parents=True, exist_ok=True)

    names = list_bundled_agents()
    first_name = names[0]
    src = bundled_agent_path(first_name)
    assert src is not None

    # Pre-deploy the first agent so it exists (sync won't touch it)
    import shutil

    shutil.copy2(src, agents_dir / f"{first_name}.md")

    partial = {
        "agents": {first_name: src.stat().st_mtime},
        "skills": {},
        "templates": {},
    }
    registry_path.write_text(yaml.dump(partial))

    updated = sync_marketplace(tmp_path)

    # All agents must now exist
    for name in names:
        deployed = agents_dir / f"{name}.md"
        assert deployed.exists(), f"{name}.md not deployed"

    # Agents not in the registry should be in the updated list
    for name in names:
        if name != first_name:
            assert name in updated

    # The first agent (with matching mtime) should NOT be in updated
    assert first_name not in updated
