"""Tests for ievo.core.registry."""

from __future__ import annotations

import asyncio
from pathlib import Path
from unittest.mock import patch

from ievo.core.registry import Registry, RegistryEntry

from .conftest import SAMPLE_REGISTRY_YAML


def test_registry_empty() -> None:
    reg = Registry()
    assert reg.list_all() == []
    assert reg.get("anything") is None


def test_registry_get_and_list() -> None:
    entries = {
        "coder": RegistryEntry(
            name="coder",
            version="0.1.0",
            description="TDD",
            model="sonnet",
            dependencies=["architect"],
        ),
    }
    reg = Registry(entries)
    assert reg.get("coder") is not None
    assert reg.get("coder").name == "coder"
    assert reg.get("nonexistent") is None
    assert len(reg.list_all()) == 1


def test_agent_download_url() -> None:
    reg = Registry()
    url = reg.agent_download_url("spec-writer")
    assert "spec-writer" in url
    assert url.startswith("https://")


def test_load_cached_returns_bundled() -> None:
    """load_cached always returns the bundled registry shipped with the package."""
    from ievo.marketplace import list_bundled_agents

    reg = Registry.load_cached()

    assert len(reg.list_all()) == len(list_bundled_agents())
    assert reg.get("spec-writer") is not None
    assert reg.get("spec-writer").model == "sonnet"


def test_registry_entry_defaults() -> None:
    entry = RegistryEntry(
        name="test",
        version="0.1.0",
        description="",
        model="sonnet",
        dependencies=[],
    )
    assert entry.category == "core"
    assert entry.file == ""
    assert entry.mandatory is False


def test_registry_entry_new_fields() -> None:
    """RegistryEntry supports file and mandatory fields."""
    entry = RegistryEntry(
        name="spec-writer",
        version="0.2.0",
        description="Spec writer",
        model="sonnet",
        dependencies=[],
        file="agents/spec-writer.md",
        mandatory=True,
    )
    assert entry.file == "agents/spec-writer.md"
    assert entry.mandatory is True


def test_registry_entry_backwards_compatible() -> None:
    """Old-style entry without file/mandatory still works."""
    entry = RegistryEntry(
        name="old",
        version="0.1.0",
        description="",
        model="sonnet",
        dependencies=[],
    )
    assert entry.file == ""
    assert entry.mandatory is False


def test_parse_registry_file(tmp_path: Path) -> None:
    """_parse_registry_file parses YAML into Registry."""
    (tmp_path / "reg.yaml").write_text(SAMPLE_REGISTRY_YAML)
    reg = Registry._parse_registry_file(tmp_path / "reg.yaml")
    assert len(reg.list_all()) == 3
    assert reg.get("spec-writer") is not None


def test_parse_registry_file_new_fields(tmp_path: Path) -> None:
    """_parse_registry_file parses file and mandatory fields."""
    (tmp_path / "reg.yaml").write_text(SAMPLE_REGISTRY_YAML)
    reg = Registry._parse_registry_file(tmp_path / "reg.yaml")
    entry = reg.get("spec-writer")
    assert entry.file == "agents/spec-writer.md"
    assert entry.mandatory is True


def test_download_agent_md_success(tmp_path: Path) -> None:
    """download_agent_md writes .md file to dest_dir."""
    entries = {
        "spec-writer": RegistryEntry(
            name="spec-writer",
            version="0.1.0",
            description="test",
            model="sonnet",
            dependencies=[],
            file="agents/spec-writer.md",
        ),
    }
    reg = Registry(entries)
    dest = tmp_path / "agents"

    class MockResponse:
        def __init__(self) -> None:
            self.status_code = 200
            self.text = "---\nname: spec-writer\n---\n# Spec Writer"

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> MockResponse:
            return MockResponse()

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    with patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()):
        result = asyncio.run(reg.download_agent_md("spec-writer", dest))

    assert result is True
    assert (dest / "spec-writer.md").exists()
    assert "spec-writer" in (dest / "spec-writer.md").read_text()


def test_download_agent_md_not_in_registry(tmp_path: Path) -> None:
    """download_agent_md returns False for unknown agent."""
    reg = Registry()
    result = asyncio.run(reg.download_agent_md("nonexistent", tmp_path))
    assert result is False


def test_download_agent_md_http_error(tmp_path: Path) -> None:
    """download_agent_md returns False on HTTP error."""
    import httpx as httpx_mod

    entries = {
        "broken": RegistryEntry(
            name="broken",
            version="0.1.0",
            description="",
            model="sonnet",
            dependencies=[],
            file="agents/broken.md",
        ),
    }
    reg = Registry(entries)

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> None:
            raise httpx_mod.HTTPError("Connection failed")

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    with patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()):
        result = asyncio.run(reg.download_agent_md("broken", tmp_path))

    assert result is False


def test_download_agent_md_non_200(tmp_path: Path) -> None:
    """download_agent_md returns False on non-200 response."""
    entries = {
        "broken": RegistryEntry(
            name="broken",
            version="0.1.0",
            description="",
            model="sonnet",
            dependencies=[],
        ),
    }
    reg = Registry(entries)

    class MockResponse:
        def __init__(self) -> None:
            self.status_code = 404
            self.text = "Not Found"

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> MockResponse:
            return MockResponse()

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    with patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()):
        result = asyncio.run(reg.download_agent_md("broken", tmp_path))

    assert result is False


def test_download_agent_md_no_file_field(tmp_path: Path) -> None:
    """download_agent_md falls back to agents/{name}.md when file field is empty."""
    entries = {
        "test": RegistryEntry(
            name="test",
            version="0.1.0",
            description="",
            model="sonnet",
            dependencies=[],
            file="",
        ),
    }
    reg = Registry(entries)
    dest = tmp_path / "agents"

    requested_urls: list[str] = []

    class MockResponse:
        def __init__(self) -> None:
            self.status_code = 200
            self.text = "---\nname: test\n---\n"

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> MockResponse:
            requested_urls.append(url)
            return MockResponse()

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    with patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()):
        result = asyncio.run(reg.download_agent_md("test", dest))

    assert result is True
    assert any("agents/test.md" in url for url in requested_urls)


def test_download_agent_discovers_all_files(tmp_path: Path) -> None:
    """download_agent uses Contents API to discover files dynamically."""
    entries = {
        "spec-writer": RegistryEntry(
            name="spec-writer",
            version="0.1.0",
            description="test",
            model="sonnet",
            dependencies=[],
        ),
    }
    reg = Registry(entries)
    dest = tmp_path / "agents" / "spec-writer"

    # Mock Contents API response
    api_items = [
        {
            "type": "file",
            "name": "agent.yaml",
            "path": "agents/spec-writer/agent.yaml",
            "download_url": "https://raw/agent.yaml",
        },
        {
            "type": "file",
            "name": "ROLE.md",
            "path": "agents/spec-writer/ROLE.md",
            "download_url": "https://raw/ROLE.md",
        },
        {
            "type": "file",
            "name": "EVOLUTION_LOG.md",
            "path": "agents/spec-writer/EVOLUTION_LOG.md",
            "download_url": "https://raw/EVOLUTION_LOG.md",
        },
        {
            "type": "dir",
            "name": "templates",
            "path": "agents/spec-writer/templates",
            "url": "https://api/templates",
        },
    ]
    template_items = [
        {
            "type": "file",
            "name": "REQ.md",
            "path": "agents/spec-writer/templates/REQ.md",
            "download_url": "https://raw/REQ.md",
        },
    ]

    mock_responses = {
        "https://api.github.com/repos/ievo-ai/marketplace/contents/agents/spec-writer": (
            200,
            api_items,
        ),
        "https://api/templates": (200, template_items),
    }
    file_contents = {
        "https://raw/agent.yaml": "name: spec-writer",
        "https://raw/ROLE.md": "# Spec Writer",
        "https://raw/EVOLUTION_LOG.md": "# Evolution Log",
        "https://raw/REQ.md": "# REQ Template",
    }

    class MockResponse:
        def __init__(self, status_code: int, data: str | list) -> None:
            self.status_code = status_code
            self._data = data
            self.text = data if isinstance(data, str) else ""

        def json(self) -> list:
            return self._data

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> MockResponse:
            if url in mock_responses:
                code, data = mock_responses[url]
                return MockResponse(code, data)
            if url in file_contents:
                return MockResponse(200, file_contents[url])
            return MockResponse(404, "")

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    with patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()):
        result = asyncio.run(reg.download_agent("spec-writer", dest))

    assert result is True
    assert (dest / "agent.yaml").exists()
    assert (dest / "ROLE.md").exists()
    assert (dest / "EVOLUTION_LOG.md").exists()
    assert (dest / "templates" / "REQ.md").exists()
    assert (dest / "templates" / "REQ.md").read_text() == "# REQ Template"


def test_download_agent_no_entry(tmp_path: Path) -> None:
    """download_agent returns False if agent not in registry."""
    reg = Registry()
    result = asyncio.run(reg.download_agent("nonexistent", tmp_path / "out"))
    assert result is False


def test_download_agent_api_failure(tmp_path: Path) -> None:
    """download_agent returns False if API returns error."""
    entries = {
        "broken": RegistryEntry(
            name="broken", version="0.1.0", description="", model="sonnet", dependencies=[]
        ),
    }
    reg = Registry(entries)

    class MockResponse:
        def __init__(self) -> None:
            self.status_code = 403

        def json(self) -> list:
            return []

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> MockResponse:
            return MockResponse()

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    with patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()):
        result = asyncio.run(reg.download_agent("broken", tmp_path / "out"))

    assert result is False


def test_list_files_recursive_handles_non_list_response() -> None:
    """_list_files_recursive handles non-list API response gracefully."""

    class MockResponse:
        def __init__(self) -> None:
            self.status_code = 200

        def json(self) -> dict:
            return {"message": "Not Found"}

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> MockResponse:
            return MockResponse()

    result = asyncio.run(Registry._list_files_recursive(MockClient(), "https://api/test"))
    assert result == []


def test_fetch_success(tmp_path: Path) -> None:
    """fetch() downloads and parses registry."""
    cache_dir = tmp_path / "cache"
    cache_dir.mkdir()

    class MockResponse:
        def __init__(self) -> None:
            self.status_code = 200
            self.text = SAMPLE_REGISTRY_YAML

        def raise_for_status(self) -> None:
            pass

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> MockResponse:
            return MockResponse()

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    reg = Registry()
    with (
        patch("ievo.core.registry.CACHE_DIR", cache_dir),
        patch("ievo.core.registry.ensure_home"),
        patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()),
    ):
        asyncio.run(reg.fetch())

    assert len(reg.list_all()) == 3
    assert reg.get("spec-writer") is not None
    assert (cache_dir / "registry.yaml").exists()


def test_fetch_http_error(tmp_path: Path) -> None:
    """fetch() handles HTTP errors gracefully."""
    import httpx

    cache_dir = tmp_path / "cache"
    cache_dir.mkdir()

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> None:
            raise httpx.HTTPError("Connection failed")

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    reg = Registry()
    with (
        patch("ievo.core.registry.CACHE_DIR", cache_dir),
        patch("ievo.core.registry.ensure_home"),
        patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()),
    ):
        asyncio.run(reg.fetch())

    # Should still work (empty) — no crash
    assert reg.list_all() == []


def test_download_agent_http_error_during_file(tmp_path: Path) -> None:
    """download_agent handles HTTPError when downloading individual files."""
    import httpx

    entries = {
        "spec-writer": RegistryEntry(
            name="spec-writer", version="0.1.0", description="", model="sonnet", dependencies=[]
        ),
    }
    reg = Registry(entries)
    dest = tmp_path / "agents" / "spec-writer"

    class MockResponse:
        def __init__(self, status_code: int, data: str | list) -> None:
            self.status_code = status_code
            self._data = data
            self.text = data if isinstance(data, str) else ""

        def json(self) -> list:
            return self._data

    api_items = [
        {
            "type": "file",
            "name": "agent.yaml",
            "path": "agents/spec-writer/agent.yaml",
            "download_url": "https://raw/agent.yaml",
        },
    ]

    call_count = 0

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> MockResponse:
            nonlocal call_count
            call_count += 1
            if "api.github.com" in url:
                return MockResponse(200, api_items)
            raise httpx.HTTPError("Download failed")

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    with patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()):
        result = asyncio.run(reg.download_agent("spec-writer", dest))

    assert result is True  # Returns True even if individual files fail


def test_download_agent_file_non_200(tmp_path: Path) -> None:
    """download_agent skips files with non-200 response (branch: 105->102)."""
    entries = {
        "spec-writer": RegistryEntry(
            name="spec-writer", version="0.1.0", description="", model="sonnet", dependencies=[]
        ),
    }
    reg = Registry(entries)
    dest = tmp_path / "agents" / "spec-writer"

    class MockResponse:
        def __init__(self, status_code: int, data: str | list) -> None:
            self.status_code = status_code
            self._data = data
            self.text = data if isinstance(data, str) else ""

        def json(self) -> list:
            return self._data

    api_items = [
        {
            "type": "file",
            "name": "agent.yaml",
            "path": "agents/spec-writer/agent.yaml",
            "download_url": "https://raw/agent.yaml",
        },
    ]

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> MockResponse:
            if "api.github.com" in url:
                return MockResponse(200, api_items)
            # File download returns 403
            return MockResponse(403, "Forbidden")

        async def __aenter__(self) -> MockClient:
            return self

        async def __aexit__(self, *args: object) -> None:
            pass

    with patch("ievo.core.registry.httpx.AsyncClient", return_value=MockClient()):
        result = asyncio.run(reg.download_agent("spec-writer", dest))

    assert result is True
    # File should NOT be written
    assert not (dest / "agent.yaml").exists()


def test_list_files_recursive_exception() -> None:
    """_list_files_recursive handles general exceptions."""

    class MockClient:
        async def get(self, url: str, **kwargs: object) -> None:
            raise RuntimeError("Something went wrong")

    result = asyncio.run(Registry._list_files_recursive(MockClient(), "https://api/test"))
    assert result == []
