"""Tests for CLI persona loading utilities.

Covers JSON and markdown persona file parsing, including
frontmatter extraction and fallback behaviors.
"""

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

import pytest
from click.testing import CliRunner

from anima.shell.cli_utils import (
    _load_and_validate_persona,
    load_persona_or_synthesize,
)


# =============================================================================
# Test Fixtures
# =============================================================================


@pytest.fixture
def cli_runner() -> CliRunner:
    """Create a CliRunner instance for testing."""
    return CliRunner()


@pytest.fixture
def tmp_persona_dir(tmp_path: Path) -> Path:
    """Create temporary directory for persona files."""
    return tmp_path / "personas"


# =============================================================================
# JSON Persona Tests
# =============================================================================


def test_load_persona_json_still_works(tmp_persona_dir: Path):
    """Test that JSON persona files still load correctly."""
    tmp_persona_dir.mkdir()
    json_file = tmp_persona_dir / "agent.json"
    json_file.write_text(
        json.dumps(
            {
                "id": "json-agent",
                "model": "gpt-5.2",
                "prompt": "You are a JSON-based agent.",
            }
        )
    )

    result = _load_and_validate_persona(json_file)

    assert result.id == "json-agent"
    assert result.model == "gpt-5.2"
    assert result.prompt == "You are a JSON-based agent."
    assert result.spec_version == "0.1.0"


def test_load_persona_json_prompt_precedence_over_legacy_system_prompt(
    tmp_persona_dir: Path,
) -> None:
    """When both prompt and system_prompt exist, canonical prompt wins."""
    tmp_persona_dir.mkdir()
    json_file = tmp_persona_dir / "agent_precedence.json"
    json_file.write_text(
        json.dumps(
            {
                "id": "precedence-agent",
                "model": "gpt-5.2",
                "prompt": "canonical prompt",
                "system_prompt": "legacy prompt should be ignored",
            }
        )
    )

    result = _load_and_validate_persona(json_file)

    assert result.prompt == "canonical prompt"


# =============================================================================
# Markdown Persona Tests
# =============================================================================


def test_load_persona_markdown_with_frontmatter(tmp_persona_dir: Path):
    """Test markdown persona with YAML frontmatter."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "assistant.md"
    md_file.write_text("""---
id: markdown-assistant
model: openai:gpt-5.2
---

You are a helpful assistant specialized in markdown.
""")

    result = _load_and_validate_persona(md_file)

    assert result.id == "markdown-assistant"
    assert result.model == "openai:gpt-5.2"
    assert result.prompt == "You are a helpful assistant specialized in markdown."
    assert result.spec_version == "0.1.0"


def test_load_persona_markdown_no_frontmatter(tmp_persona_dir: Path):
    """Test markdown without frontmatter fails validation (model required)."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "custom-agent.md"
    md_file.write_text("""You are a custom agent with no frontmatter.
Your primary task is to assist users.""")

    # PersonaSpec requires 'model', should raise ValidationError -> SystemExit
    with pytest.raises(SystemExit):
        _load_and_validate_persona(md_file)


def test_load_persona_markdown_fallback_name(tmp_persona_dir: Path):
    """Test that fallback_name parameter uses file stem."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "worker-bot.md"
    md_file.write_text("""---
model: gpt-4
---

You are a worker bot.""")

    result = _load_and_validate_persona(md_file)

    # Frontmatter lacks 'id', should use file stem
    assert result.id == "worker-bot"
    assert result.model == "gpt-4"
    assert result.prompt == "You are a worker bot."


def test_load_persona_markdown_with_extra_frontmatter_fields(tmp_persona_dir: Path):
    """Test that unknown frontmatter fields are ignored (not an error)."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "advanced.md"
    md_file.write_text("""---
id: advanced-agent
model: openai:gpt-5.2
version: 1.0
author: Test Suite
extra_field: ignored
---

You are an advanced agent.""")

    # Unknown fields should be filtered out, not cause validation error
    result = _load_and_validate_persona(md_file)

    assert result.id == "advanced-agent"
    assert result.model == "openai:gpt-5.2"
    assert result.prompt == "You are an advanced agent."
    # Unknown fields (version, author, extra_field) are not in PersonaSpec
    assert not hasattr(result, "version")
    assert not hasattr(result, "author")
    assert not hasattr(result, "extra_field")


def test_load_persona_markdown_empty_body(tmp_persona_dir: Path):
    """Test markdown with frontmatter but empty body fails validation."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "empty-body.md"
    md_file.write_text("""---
id: empty-body-agent
model: gpt-4
---
""")

    with pytest.raises(SystemExit):
        _load_and_validate_persona(md_file)


def test_load_persona_markdown_frontmatter_only_name(tmp_persona_dir: Path):
    """Test markdown where frontmatter only specifies id (no model)."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "minimal.md"
    md_file.write_text("""---
id: minimal-agent
---

You are a minimal agent.""")

    # PersonaSpec requires 'model', should raise ValidationError -> SystemExit
    with pytest.raises(SystemExit):
        _load_and_validate_persona(md_file)


# =============================================================================
# Error Handling Tests
# =============================================================================


def test_load_persona_file_not_found():
    """Test that missing file exits with error."""
    nonexistent = Path("/nonexistent/path/to/persona.json")

    # Should exit with SystemExit
    with pytest.raises(SystemExit):
        _load_and_validate_persona(nonexistent)


def test_load_persona_invalid_json_but_not_markdown(tmp_persona_dir: Path):
    """Test that non-JSON content falls back to markdown parsing."""
    tmp_persona_dir.mkdir()
    # File content that's invalid JSON but valid markdown (with model)
    md_file = tmp_persona_dir / "fallback.md"
    md_file.write_text("""---
model: gpt-4
---

This is plain markdown content that cannot be parsed as JSON.""")

    # Should fall back to markdown parsing
    result = _load_and_validate_persona(md_file)

    assert result.id == "fallback"  # Uses file stem
    assert result.model == "gpt-4"
    assert (
        result.prompt == "This is plain markdown content that cannot be parsed as JSON."
    )


def test_load_persona_markdown_opencode_description_no_body(tmp_persona_dir: Path):
    """Test OpenCode-style description field (no body)."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "opencode-agent.md"
    md_file.write_text("""---
model: gpt-4
description: A helpful assistant for coding
---

""")

    result = _load_and_validate_persona(md_file)

    assert result.id == "opencode-agent"  # Fallback to filename
    assert result.model == "gpt-4"
    # Description used as system_prompt when body is empty
    assert result.prompt == "A helpful assistant for coding"


def test_load_persona_markdown_opencode_description_with_body(tmp_persona_dir: Path):
    """Test OpenCode-style description field with body (body takes precedence)."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "opencode-agent.md"
    md_file.write_text("""---
model: gpt-4
description: Ignored description
---

Use this body content instead.""")

    result = _load_and_validate_persona(md_file)

    assert result.id == "opencode-agent"
    assert result.model == "gpt-4"
    # Body takes precedence over description
    assert result.prompt == "Use this body content instead."


# =============================================================================
# Non-Mapping YAML Frontmatter Tests
# =============================================================================


def test_load_persona_markdown_list_frontmatter(tmp_persona_dir: Path):
    """Test that list YAML frontmatter doesn't crash (treated as no frontmatter)."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "bad-list.md"
    md_file.write_text("""---
- item1
- item2
---

You are an agent.""")

    # Should not crash, treat entire content as body
    with pytest.raises(SystemExit):
        # SystemExit because no 'model' field in frontmatter (treated as absent)
        _load_and_validate_persona(md_file)


def test_load_persona_markdown_list_frontmatter_with_model(tmp_persona_dir: Path):
    """Test list YAML frontmatter falls back to pure markdown with filename stem."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "list-agent.md"
    md_file.write_text("""---
- item1
- item2
---

You are an agent with list frontmatter, but model must come from CLI args.""")

    # List frontmatter is treated as "no frontmatter", so entire content is body
    # This will fail validation (model required), but should not crash with AttributeError
    with pytest.raises(SystemExit):
        _load_and_validate_persona(md_file)


def test_load_persona_markdown_scalar_frontmatter(tmp_persona_dir: Path):
    """Test that scalar YAML frontmatter (e.g., just a string) doesn't crash."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "scalar.md"
    md_file.write_text("""---
just a string
---

You are an agent.""")

    # Should not crash, treat entire content as body
    # Will fail validation (model required), but gracefully
    with pytest.raises(SystemExit):
        _load_and_validate_persona(md_file)


def test_load_persona_markdown_number_frontmatter(tmp_persona_dir: Path):
    """Test that numeric YAML frontmatter doesn't crash."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "number.md"
    md_file.write_text("""---
42
---

You are an agent.""")

    # Should not crash
    with pytest.raises(SystemExit):
        _load_and_validate_persona(md_file)


def test_load_persona_markdown_null_frontmatter(tmp_persona_dir: Path):
    """Test that null YAML frontmatter doesn't crash."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "null.md"
    md_file.write_text("""---
null
---

You are an agent.""")

    # Should not crash
    with pytest.raises(SystemExit):
        _load_and_validate_persona(md_file)


def test_load_persona_markdown_with_utf8_bom(tmp_persona_dir: Path):
    """Test that UTF-8 BOM prefix is stripped before parsing."""
    tmp_persona_dir.mkdir()
    md_file = tmp_persona_dir / "bom-agent.md"
    md_file.write_bytes(
        b"\xef\xbb\xbf---\nid: bom-agent\nmodel: gpt-4\n---\nHello from BOM file."
    )

    result = _load_and_validate_persona(md_file)

    assert result.id == "bom-agent"
    assert result.model == "gpt-4"
    assert result.prompt == "Hello from BOM file."


def test_frontmatter_fields_subset_of_persona_spec():
    """Ensure PERSONA_SPEC_FRONTMATTER_FIELDS stays in sync with PersonaSpec."""
    from anima.core.persona_import import PERSONA_SPEC_FRONTMATTER_FIELDS
    from anima.core.schemas import PersonaSpec

    persona_fields = set(PersonaSpec.model_fields.keys())
    drift = PERSONA_SPEC_FRONTMATTER_FIELDS - persona_fields
    assert not drift, (
        f"PERSONA_SPEC_FRONTMATTER_FIELDS has fields not in PersonaSpec: {drift}. "
        f"Update persona_import.py to match PersonaSpec."
    )


def test_load_persona_or_synthesize_uses_default_prompt_when_none(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Inline synthesis provides a non-empty canonical prompt when omitted."""

    class _Defaults:
        model = "gpt-5.2"

    class _Config:
        defaults = _Defaults()

    from returns.result import Success

    monkeypatch.setattr(
        "anima.shell.cli_utils.load_global_config",
        lambda: Success(_Config()),
    )

    persona = load_persona_or_synthesize(
        persona=None,
        system_prompt=None,
        model="gpt-5.2",
    )

    assert persona.prompt == "You are a helpful assistant."
    assert persona.tools == {}
