"""Tests for OpenSpecService with mocked subprocess calls."""

from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest

from hangar.services.openspec import OpenSpecService

FIXTURES = Path(__file__).parent / "fixtures"


@pytest.fixture
def service(tmp_path: Path) -> OpenSpecService:
    """Create an OpenSpecService with a temporary project path."""
    return OpenSpecService(project_path=tmp_path)


@pytest.fixture
def service_with_artifacts(tmp_path: Path) -> OpenSpecService:
    """Create service with actual artifact files on disk."""
    changes_dir = tmp_path / "openspec" / "changes" / "test-change"
    changes_dir.mkdir(parents=True)

    (changes_dir / "proposal.md").write_text("# Proposal\n\nTest proposal content.")
    (changes_dir / "design.md").write_text("# Design\n\nTest design content.")

    specs_dir = changes_dir / "specs"
    specs_dir.mkdir()
    (specs_dir / "core.md").write_text("# Core Spec\n\nTest spec content.")

    return OpenSpecService(project_path=tmp_path)


def _mock_process(stdout: str, returncode: int = 0) -> AsyncMock:
    """Create a mock subprocess process."""
    proc = AsyncMock()
    proc.communicate.return_value = (stdout.encode(), b"")
    proc.returncode = returncode
    return proc


@pytest.mark.asyncio
async def test_list_changes(service: OpenSpecService) -> None:
    """list_changes should run openspec list --json and parse output."""
    fixture_data = (FIXTURES / "openspec_list.json").read_text()

    with patch("asyncio.create_subprocess_exec", return_value=_mock_process(fixture_data)) as mock_exec:
        result = await service.list_changes()

        mock_exec.assert_called_once_with(
            "openspec", "list", "--json",
            stdout=-1, stderr=-1, cwd=service.project_path,
        )
        assert len(result.changes) > 0
        assert result.changes[0].name == "mvp-foundation"


@pytest.mark.asyncio
async def test_show_change(service: OpenSpecService) -> None:
    """show_change should run openspec status --change <name> --json."""
    fixture_data = (FIXTURES / "openspec_status.json").read_text()
    # openspec status may include non-JSON prefix lines
    raw_output = "- Loading change status...\n" + fixture_data

    with patch("asyncio.create_subprocess_exec", return_value=_mock_process(raw_output)) as mock_exec:
        result = await service.show_change("mvp-foundation")

        mock_exec.assert_called_once_with(
            "openspec", "status", "--change", "mvp-foundation", "--json",
            stdout=-1, stderr=-1, cwd=service.project_path,
        )
        assert result.change_name == "mvp-foundation"
        assert len(result.artifacts) > 0


@pytest.mark.asyncio
async def test_read_artifact_file(service_with_artifacts: OpenSpecService) -> None:
    """read_artifact should read markdown file content from disk."""
    result = await service_with_artifacts.read_artifact("test-change", "proposal.md")

    assert result.id == "proposal"
    assert result.output_path == "proposal.md"
    assert "# Proposal" in result.content
    assert result.status == "done"


@pytest.mark.asyncio
async def test_read_artifact_glob(service_with_artifacts: OpenSpecService) -> None:
    """read_artifact should handle glob patterns (specs/**/*.md)."""
    result = await service_with_artifacts.read_artifact("test-change", "specs/**/*.md")

    assert result.id == "specs"
    assert "# Core Spec" in result.content


@pytest.mark.asyncio
async def test_read_artifact_missing(service_with_artifacts: OpenSpecService) -> None:
    """read_artifact should return empty content for missing files."""
    result = await service_with_artifacts.read_artifact("test-change", "nonexistent.md")

    assert result.content == ""
    assert result.status == "missing"


@pytest.mark.asyncio
async def test_openspec_command_failure(service: OpenSpecService) -> None:
    """Service should raise RuntimeError when openspec command fails."""
    proc = AsyncMock()
    proc.communicate.return_value = (b"", b"Error: change not found")
    proc.returncode = 1

    with patch("asyncio.create_subprocess_exec", return_value=proc):
        with pytest.raises(RuntimeError, match="openspec .* failed"):
            await service.list_changes()


@pytest.mark.asyncio
async def test_list_changes_includes_archived(tmp_path: Path) -> None:
    """list_changes should include archived changes from the archive directory."""
    # Set up archive directory with an archived change
    archive_dir = tmp_path / "openspec" / "changes" / "archive" / "old-feature"
    archive_dir.mkdir(parents=True)
    (archive_dir / "proposal.md").write_text("# Archived proposal")

    service = OpenSpecService(project_path=tmp_path)
    fixture_data = (FIXTURES / "openspec_list.json").read_text()

    with patch("asyncio.create_subprocess_exec", return_value=_mock_process(fixture_data)):
        result = await service.list_changes()

        # Should have active changes + archived
        active = [c for c in result.changes if not c.is_archived]
        archived = [c for c in result.changes if c.is_archived]
        assert len(active) > 0
        assert len(archived) == 1
        assert archived[0].name == "old-feature"
        assert archived[0].status == "archived"


@pytest.mark.asyncio
async def test_list_changes_no_archive_dir(service: OpenSpecService) -> None:
    """list_changes should work fine when no archive directory exists."""
    fixture_data = (FIXTURES / "openspec_list.json").read_text()

    with patch("asyncio.create_subprocess_exec", return_value=_mock_process(fixture_data)):
        result = await service.list_changes()
        # All should be active (no archive dir)
        assert all(not c.is_archived for c in result.changes)


@pytest.mark.asyncio
async def test_read_artifact_from_archive(tmp_path: Path) -> None:
    """read_artifact should fallback to archive directory when change not in changes/."""
    # Set up only in archive directory
    archive_dir = tmp_path / "openspec" / "changes" / "archive" / "old-feature"
    archive_dir.mkdir(parents=True)
    (archive_dir / "proposal.md").write_text("# Archived Proposal\n\nOld content.")

    # Also create the changes dir (empty)
    (tmp_path / "openspec" / "changes").mkdir(parents=True, exist_ok=True)

    service = OpenSpecService(project_path=tmp_path)
    result = await service.read_artifact("old-feature", "proposal.md")

    assert result.status == "done"
    assert "# Archived Proposal" in result.content
