from __future__ import annotations

from typing import Any

import pytest

from resume_agent.agent import (
    build_profile,
    revise_tailored_resume,
    tailor_resume,
    validate_package,
)
from resume_agent.models import (
    BackedText,
    CareerProfile,
    ContactInfo,
    JobAnalysis,
    MatchAssessment,
    ResumeSection,
    TailoredResume,
    TailoringPackage,
)

CONTACT = ContactInfo(
    full_name="Ada Example",
    email="ada@example.com",
    phone=None,
    location="London",
    linkedin=None,
    website=None,
)


class FakeLLM:
    def __init__(self, responses: list[Any]) -> None:
        self.responses = responses
        self.calls: list[tuple[str, str]] = []

    def parse(self, schema: type[Any], instructions: str, input_text: str) -> Any:
        self.calls.append((instructions, input_text))
        response = self.responses.pop(0)
        assert isinstance(response, schema)
        return response


def profile() -> CareerProfile:
    return CareerProfile.model_validate(
        {
            "contact": CONTACT.model_dump(),
            "professional_identity": "Backend engineer",
            "differentiators": ["Reliable distributed systems"],
            "target_roles": ["Senior Backend Engineer"],
            "facts": [
                {
                    "id": "F001",
                    "category": "experience",
                    "statement": "Reduced API latency by 30%.",
                    "source_name": "resume",
                    "source_excerpt": "Reduced API latency by 30%.",
                }
            ],
            "skills": ["Python"],
            "unanswered_questions": [],
        }
    )


def package(evidence_ids: list[str] | None = None) -> TailoringPackage:
    item = BackedText(
        text="Reduced API latency by 30%.",
        evidence_ids=evidence_ids if evidence_ids is not None else ["F001"],
    )
    return TailoringPackage(
        job=JobAnalysis(
            company="Example Co",
            role_title="Backend Engineer",
            mission=None,
            requirements=[],
            responsibilities=[],
            culture_signals=[],
            ats_keywords=["Python"],
        ),
        resume=TailoredResume(
            contact=CONTACT,
            headline="Backend Engineer",
            summary=[item],
            sections=[ResumeSection(title="Experience", items=[item])],
        ),
        match=MatchAssessment(
            strong_matches=["API performance"],
            partial_matches=[],
            genuine_gaps=[],
            keywords_used=["Python"],
        ),
        changes_made=["Prioritized relevant impact."],
        questions_for_candidate=[],
        warnings=[],
    )


def test_build_profile_uses_structured_result() -> None:
    expected = profile()
    assert build_profile(FakeLLM([expected]), "resume text") == expected


def test_build_profile_repairs_an_empty_fact_ledger() -> None:
    empty = profile().model_copy(update={"facts": []})
    expected = profile()

    assert build_profile(FakeLLM([empty, expected]), "resume text") == expected


def test_build_profile_reports_empty_fact_ledger_after_repair() -> None:
    empty = profile().model_copy(update={"facts": []})

    with pytest.raises(ValueError, match="after two attempts"):
        build_profile(FakeLLM([empty, empty]), "resume text")


def test_tailor_runs_draft_and_audit() -> None:
    expected = package()
    result = tailor_resume(FakeLLM([expected, expected]), profile(), "long job post")
    assert result == expected


def test_tailoring_strength_controls_rewriting_without_relaxing_evidence() -> None:
    expected = package()
    llm = FakeLLM([expected, expected])

    tailor_resume(llm, profile(), "long job post", tailoring_strength=100)

    draft_instructions, draft_input = llm.calls[0]
    assert "Tailoring strength: 100/100" in draft_instructions
    assert "never permits fabricated" in draft_instructions
    assert '"tailoring_strength": 100' in draft_input


def test_tailoring_strength_rejects_out_of_range_values() -> None:
    with pytest.raises(ValueError, match="between 0 and 100"):
        tailor_resume(FakeLLM([]), profile(), "long job post", tailoring_strength=101)


def test_profile_evidence_mode_allows_claims_without_fact_ids() -> None:
    flexible = package()
    for item in flexible.resume.summary:
        item.evidence_ids = []
    for section in flexible.resume.sections:
        for item in section.items:
            item.evidence_ids = []
    llm = FakeLLM([flexible, flexible])

    result = tailor_resume(
        llm,
        profile(),
        "long job post",
        evidence_mode="profile",
    )

    assert result.resume.summary[0].evidence_ids == []
    assert "Per-claim evidence IDs are optional" in llm.calls[1][0]


def test_strict_evidence_mode_still_rejects_missing_fact_ids() -> None:
    flexible = package()
    flexible.resume.summary[0].evidence_ids = []

    with pytest.raises(ValueError, match="no evidence"):
        tailor_resume(FakeLLM([flexible]), profile(), "long job post")


def test_revision_chat_rewrites_and_audits_current_package() -> None:
    current = package()
    revised = package()
    revised.changes_made = ["Made experience bullets more concise."]
    llm = FakeLLM([revised, revised])

    result = revise_tailored_resume(
        llm,
        profile(),
        current,
        "Make the experience bullets more concise.",
    )

    assert result.changes_made == ["Made experience bullets more concise."]
    assert "candidate_request" in llm.calls[0][1]
    assert "only source of candidate facts" in llm.calls[0][0]


def test_revision_chat_cannot_change_job_analysis() -> None:
    current = package()
    revised = package()
    revised.job.role_title = "Different role"

    with pytest.raises(ValueError, match="cannot change"):
        revise_tailored_resume(FakeLLM([revised]), profile(), current, "Rewrite it.")


def test_unknown_evidence_is_rejected() -> None:
    with pytest.raises(ValueError, match="unknown evidence"):
        validate_package(profile(), package(["F999"]))


def test_contact_mutation_is_rejected() -> None:
    result = package()
    result.resume.contact = ContactInfo(
        full_name="Someone Else",
        email="ada@example.com",
        phone=None,
        location="London",
        linkedin=None,
        website=None,
    )
    with pytest.raises(ValueError, match="contact"):
        validate_package(profile(), result)
