"""Tests for prompt_builder.types"""

import pytest
from prompt_builder import (
    Prompt,
    Instruction,
    Persona,
    Rule,
    ContextBlock,
    Example,
    InputData,
    OutputSpec,
    CoTSpec,
    Message,
    TokenBudget,
    PriorityMap,
    PriorityEntry,
    CompilationResult,
    SectionReport,
    Section,
    TruncationStrategy,
    CoTStrategy,
    Visibility,
    Scope,
    RulePriority,
)


class TestPromptConstruction:
    def test_minimal_prompt(self):
        """Prompt only requires an instruction."""
        p = Prompt(instruction=Instruction(task="Do the thing."))
        assert p.instruction.task == "Do the thing."
        assert p.persona is None
        assert p.rules == []
        assert p.context == []
        assert p.examples == []
        assert p.input_data is None
        assert p.output_spec is None
        assert p.cot is None
        assert p.history == []
        assert p.version == "0.1.0"

    def test_full_prompt(self):
        """Prompt with all fields populated."""
        p = Prompt(
            persona=Persona(
                role="You are a code reviewer.",
                tone="terse",
                domain=["rust", "python"],
                anti_patterns=["don't apologize"],
            ),
            rules=[
                Rule(constraint="Never return SQL that mutates data."),
                Rule(constraint="Be concise.", priority=RulePriority.SOFT),
            ],
            context=[
                ContextBlock(content="The codebase uses async/await.", source="rag", relevance=0.9),
            ],
            examples=[
                Example(input="bad code", output="fixed code", weight=0.8),
            ],
            instruction=Instruction(
                task="Review this PR.",
                intent="Catch bugs before merge.",
                subtasks=["Check error handling", "Check types"],
                constraints=["Only review the diff"],
            ),
            input_data=InputData(content="def foo(): pass", format="code", label="diff"),
            output_spec=OutputSpec(format="json", length="under 500 tokens"),
            cot=CoTSpec(strategy=CoTStrategy.STEP_BY_STEP, visibility=Visibility.EXTRACTED),
            history=[
                Message(role="user", content="Review this please."),
                Message(role="assistant", content="I'll take a look."),
            ],
            version="1.0.0",
        )

        assert p.persona.role == "You are a code reviewer."
        assert len(p.rules) == 2
        assert p.rules[0].priority == RulePriority.HARD
        assert p.rules[1].priority == RulePriority.SOFT
        assert p.context[0].relevance == 0.9
        assert p.examples[0].weight == 0.8
        assert p.instruction.intent == "Catch bugs before merge."
        assert len(p.history) == 2
        assert p.version == "1.0.0"


class TestContextBlock:
    def test_defaults(self):
        cb = ContextBlock(content="hello")
        assert cb.relevance == 1.0
        assert cb.source == "manual"
        assert cb.token_count is None
        assert cb.metadata == {}

    def test_relevance_bounds(self):
        with pytest.raises(Exception):
            ContextBlock(content="hello", relevance=1.5)
        with pytest.raises(Exception):
            ContextBlock(content="hello", relevance=-0.1)


class TestTokenBudget:
    def test_available_calculation(self):
        b = TokenBudget(context_window=128_000, output_reserve=4096, safety_margin=200)
        assert b.available == 128_000 - 4096 - 200

    def test_defaults(self):
        b = TokenBudget(context_window=8000)
        assert b.output_reserve == 4096
        assert b.safety_margin == 200


class TestPriorityMap:
    def test_default_priority(self):
        pm = PriorityMap()
        assert pm.get_strategy(Section.CONTEXT) == TruncationStrategy.CUT_BY_RELEVANCE
        assert pm.get_strategy(Section.HISTORY) == TruncationStrategy.TRUNCATE_OLDEST
        assert pm.get_strategy(Section.PERSONA) == TruncationStrategy.NEVER_CUT
        assert pm.get_strategy(Section.INSTRUCTION) == TruncationStrategy.NEVER_CUT

    def test_cutting_order(self):
        pm = PriorityMap()
        cuttable = pm.cutting_order()
        # Should not include NEVER_CUT sections
        sections = [e.section for e in cuttable]
        assert Section.PERSONA not in sections
        assert Section.INSTRUCTION not in sections
        assert Section.CONTEXT in sections

    def test_custom_priority(self):
        pm = PriorityMap(entries=[
            PriorityEntry(section=Section.EXAMPLES, strategy=TruncationStrategy.CUT_BY_WEIGHT),
            PriorityEntry(section=Section.CONTEXT, strategy=TruncationStrategy.NEVER_CUT),
        ])
        assert pm.get_strategy(Section.CONTEXT) == TruncationStrategy.NEVER_CUT
        assert pm.get_strategy(Section.EXAMPLES) == TruncationStrategy.CUT_BY_WEIGHT


class TestCompilationResult:
    def test_headroom(self):
        r = CompilationResult(
            messages=[],
            total_tokens=1000,
            budget_available=5000,
        )
        assert r.headroom == 4000

    def test_report_output(self):
        r = CompilationResult(
            messages=[],
            total_tokens=1000,
            budget_available=5000,
            sections=[
                SectionReport(
                    section=Section.PERSONA,
                    included_tokens=50,
                    original_tokens=50,
                    items_included=1,
                    items_dropped=0,
                ),
                SectionReport(
                    section=Section.CONTEXT,
                    included_tokens=200,
                    original_tokens=500,
                    items_included=2,
                    items_dropped=3,
                ),
            ],
            warnings=["Over budget by 100 tokens. Trimming."],
        )
        report = r.report()
        assert "PERSONA" in report.lower() or "persona" in report
        assert "context" in report
        assert "⚠" in report
