from __future__ import annotations

import time
from pathlib import Path
from typing import Any

from fastapi.testclient import TestClient

from resume_agent.agent import save_json
from resume_agent.models import CareerProfile, TailoringPackage
from resume_agent.render import render_ohmycv_resume
from resume_agent.webapp import create_app


def sample_profile() -> CareerProfile:
    return CareerProfile.model_validate(
        {
            "contact": {
                "full_name": "Ada Example",
                "email": "ada@example.com",
                "phone": None,
                "location": "London",
                "linkedin": None,
                "website": None,
            },
            "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 sample_package() -> TailoringPackage:
    contact = sample_profile().contact.model_dump()
    item = {"text": "Reduced API latency by 30%. [F013]", "evidence_ids": ["F001"]}
    return TailoringPackage.model_validate(
        {
            "job": {
                "company": "Example Co",
                "role_title": "Backend Engineer",
                "mission": None,
                "requirements": [],
                "responsibilities": [],
                "culture_signals": [],
                "ats_keywords": ["Python"],
            },
            "resume": {
                "contact": contact,
                "headline": "Backend Engineer",
                "summary": [item],
                "sections": [{"title": "Experience", "items": [item]}],
            },
            "match": {
                "strong_matches": ["API performance"],
                "partial_matches": [],
                "genuine_gaps": [],
                "keywords_used": ["Python"],
            },
            "changes_made": ["Prioritized relevant impact."],
            "questions_for_candidate": [],
            "warnings": [],
        }
    )


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

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

    def list_models(self) -> list[str]:
        return ["test-model"]


def wait_for_task(client: TestClient, task_id: str) -> dict[str, Any]:
    for _ in range(100):
        response = client.get(f"/api/tasks/{task_id}")
        assert response.status_code == 200
        task = response.json()
        if task["status"] != "running":
            return task
        time.sleep(0.01)
    raise AssertionError("Background task did not finish in time.")


def test_root_and_status(tmp_path: Path, monkeypatch: Any) -> None:
    monkeypatch.delenv("RESUME_AGENT_API_KEY", raising=False)
    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    app = create_app(
        profile_path=tmp_path / "profile.json",
        output_dir=tmp_path / "output",
        llm_factory=lambda: FakeLLM([]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        assert client.get("/").status_code == 200
        favicon = client.get("/favicon.ico")
        assert favicon.status_code == 200
        assert favicon.headers["content-type"] == "image/x-icon"
        status = client.get("/api/status").json()
        assert status["configured"] is False
        assert status["profile_exists"] is False


def test_build_profile_from_upload(tmp_path: Path) -> None:
    expected = sample_profile()
    app = create_app(
        profile_path=tmp_path / "profile.json",
        output_dir=tmp_path / "output",
        llm_factory=lambda: FakeLLM([expected]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        response = client.post(
            "/api/profile",
            files={"resume": ("resume.md", b"# Ada\nReduced API latency by 30%.")},
            data={"about": ""},
        )

    assert response.status_code == 200
    assert response.json()["facts"][0]["id"] == "F001"
    assert (tmp_path / "profile.json").is_file()


def test_background_profile_upload_returns_pollable_task(tmp_path: Path) -> None:
    expected = sample_profile()
    app = create_app(
        profile_path=tmp_path / "profile.json",
        output_dir=tmp_path / "output",
        llm_factory=lambda: FakeLLM([expected]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        started = client.post(
            "/api/profile/start",
            files={"resume": ("resume.md", b"# Ada\nReduced API latency by 30%.")},
            data={"about": ""},
        )
        task = wait_for_task(client, started.json()["task_id"])

    assert started.status_code == 200
    assert task["status"] == "succeeded"
    assert task["result"]["facts"][0]["id"] == "F001"


def test_tailor_creates_downloads(tmp_path: Path) -> None:
    profile_path = tmp_path / "profile.json"
    output_dir = tmp_path / "output"
    save_json(sample_profile(), profile_path)
    expected = sample_package()
    app = create_app(
        profile_path=profile_path,
        output_dir=output_dir,
        llm_factory=lambda: FakeLLM([expected, expected]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        response = client.post(
            "/api/tailor",
            json={
                "job_url": None,
                "job_text": "Backend engineer role requiring Python and API performance. " * 3,
            },
        )
        download = client.get("/api/download/resume.md")
        ohmycv_download = client.get("/api/download/resume-ohmycv.md")

    assert response.status_code == 200
    assert response.json()["job"]["company"] == "Example Co"
    assert download.status_code == 200
    assert "Ada Example" in download.text
    assert "[F013]" not in download.text
    assert ohmycv_download.status_code == 200
    assert ohmycv_download.text.startswith('---\nname: "Ada Example"')
    assert "[F013]" not in ohmycv_download.text


def test_background_tailoring_returns_pollable_task(tmp_path: Path) -> None:
    profile_path = tmp_path / "profile.json"
    output_dir = tmp_path / "output"
    save_json(sample_profile(), profile_path)
    expected = sample_package()
    app = create_app(
        profile_path=profile_path,
        output_dir=output_dir,
        llm_factory=lambda: FakeLLM([expected, expected]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        started = client.post(
            "/api/tailor/start",
            json={
                "job_url": None,
                "job_text": "Backend engineer role requiring Python and API performance. " * 3,
            },
        )
        task = wait_for_task(client, started.json()["task_id"])

    assert started.status_code == 200
    assert task["status"] == "succeeded"
    assert task["result"]["job"]["company"] == "Example Co"
    assert (output_dir / "resume-ohmycv.md").is_file()


def test_profile_based_tailoring_allows_empty_evidence_ids(tmp_path: Path) -> None:
    profile_path = tmp_path / "profile.json"
    output_dir = tmp_path / "output"
    save_json(sample_profile(), profile_path)
    flexible = sample_package()
    for item in flexible.resume.summary:
        item.evidence_ids = []
    for section in flexible.resume.sections:
        for item in section.items:
            item.evidence_ids = []
    app = create_app(
        profile_path=profile_path,
        output_dir=output_dir,
        llm_factory=lambda: FakeLLM([flexible, flexible]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        response = client.post(
            "/api/tailor",
            json={
                "job_url": None,
                "job_text": "Backend engineer role requiring Python and API performance. " * 3,
                "evidence_mode": "profile",
            },
        )

    assert response.status_code == 200
    assert response.json()["resume"]["summary"][0]["evidence_ids"] == []
    assert (output_dir / "evidence-mode.txt").read_text() == "profile"
    assert "<!-- evidence:" not in (output_dir / "resume-audited.md").read_text()


def test_app_wide_guard_off_forces_profile_mode_and_persists(tmp_path: Path) -> None:
    profile_path = tmp_path / "profile.json"
    output_dir = tmp_path / "output"
    settings_path = tmp_path / "app-settings.json"
    save_json(sample_profile(), profile_path)
    flexible = sample_package()
    for item in flexible.resume.summary:
        item.evidence_ids = []
    for section in flexible.resume.sections:
        for item in section.items:
            item.evidence_ids = []
    app = create_app(
        profile_path=profile_path,
        output_dir=output_dir,
        settings_path=settings_path,
        llm_factory=lambda: FakeLLM([flexible, flexible]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        updated = client.put("/api/settings", json={"evidence_guard": False})
        status = client.get("/api/status")
        response = client.post(
            "/api/tailor",
            json={
                "job_url": None,
                "job_text": "Backend engineer role requiring Python and API performance. " * 3,
                "evidence_mode": "strict",
            },
        )

    assert updated.status_code == 200
    assert updated.json() == {"evidence_guard": False}
    assert status.json()["evidence_guard"] is False
    assert response.status_code == 200
    assert response.json()["resume"]["summary"][0]["evidence_ids"] == []
    assert (output_dir / "evidence-mode.txt").read_text() == "profile"
    assert '"evidence_guard": false' in settings_path.read_text()


def test_tailor_requires_exactly_one_job_source(tmp_path: Path) -> None:
    profile_path = tmp_path / "profile.json"
    save_json(sample_profile(), profile_path)
    app = create_app(
        profile_path=profile_path,
        output_dir=tmp_path / "output",
        llm_factory=lambda: FakeLLM([]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        response = client.post(
            "/api/tailor",
            json={"job_url": "https://example.com/job", "job_text": "Also pasted"},
        )

    assert response.status_code == 422


def test_tailor_rejects_out_of_range_strength(tmp_path: Path) -> None:
    profile_path = tmp_path / "profile.json"
    save_json(sample_profile(), profile_path)
    app = create_app(
        profile_path=profile_path,
        output_dir=tmp_path / "output",
        llm_factory=lambda: FakeLLM([]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        response = client.post(
            "/api/tailor",
            json={
                "job_url": None,
                "job_text": "Backend engineer role requiring Python. " * 3,
                "tailoring_strength": 101,
            },
        )

    assert response.status_code == 422


def test_revision_chat_updates_tailored_outputs(tmp_path: Path) -> None:
    profile_path = tmp_path / "profile.json"
    output_dir = tmp_path / "output"
    current = sample_package()
    revised = sample_package()
    revised.changes_made = ["Made the summary more direct."]
    save_json(sample_profile(), profile_path)
    save_json(current, output_dir / "tailoring.json")
    app = create_app(
        profile_path=profile_path,
        output_dir=output_dir,
        llm_factory=lambda: FakeLLM([revised, revised]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        response = client.post(
            "/api/revise",
            json={"message": "Make the summary more direct."},
        )
        download = client.get("/api/download/resume-ohmycv.md")

    assert response.status_code == 200
    assert response.json()["package"]["changes_made"] == [
        "Made the summary more direct."
    ]
    assert "Updated and re-audited" in response.json()["reply"]
    assert download.status_code == 200
    assert "[F013]" not in download.text


def test_revision_chat_requires_a_tailored_resume(tmp_path: Path) -> None:
    app = create_app(
        profile_path=tmp_path / "profile.json",
        output_dir=tmp_path / "output",
        llm_factory=lambda: FakeLLM([]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        response = client.post("/api/revise", json={"message": "Make it shorter."})

    assert response.status_code == 409


def test_markdown_tailor_preserves_ohmycv_front_matter(tmp_path: Path) -> None:
    expected_profile = sample_profile()
    expected_package = sample_package()
    app = create_app(
        profile_path=tmp_path / "profile.json",
        output_dir=tmp_path / "output",
        cv_dist=tmp_path / "missing-cv",
        llm_factory=lambda: FakeLLM([expected_profile, expected_package, expected_package]),  # type: ignore[arg-type]
    )
    source = """---
name: Ada Example
header:
  - text: ada@example.com
---

## Experience

- Reduced API latency by 30%.
"""

    with TestClient(app) as client:
        response = client.post(
            "/api/markdown/tailor",
            json={
                "markdown": source,
                "about": "",
                "job_url": None,
                "job_text": "Backend engineer role requiring Python and API performance. " * 3,
            },
        )

    assert response.status_code == 200
    markdown = response.json()["markdown"]
    assert markdown.startswith("---\nname: Ada Example\nheader:")
    assert "## Professional Summary\n\nReduced API latency" in markdown
    assert "Reduced API latency by 30%." in markdown
    assert "[F013]" not in markdown
    assert (tmp_path / "output/resume-ohmycv.md").read_text() == markdown


def test_ohmycv_render_creates_native_header_when_source_has_none() -> None:
    markdown = render_ohmycv_resume(sample_package(), "# Old resume")

    assert markdown.startswith('---\nname: "Ada Example"\nheader:')
    assert 'data-icon=\\"tabler:mail\\"' in markdown
    assert 'link: "mailto:ada@example.com"' in markdown
    assert "[F013]" not in markdown


def test_status_reports_built_cv_editor(tmp_path: Path) -> None:
    cv_dist = tmp_path / "cv"
    cv_dist.mkdir()
    (cv_dist / "index.html").write_text("<h1>Oh My CV</h1>", encoding="utf-8")
    (cv_dist / "200.html").write_text("<h1>Editor fallback</h1>", encoding="utf-8")
    app = create_app(
        profile_path=tmp_path / "profile.json",
        output_dir=tmp_path / "output",
        cv_dist=cv_dist,
        llm_factory=lambda: FakeLLM([]),  # type: ignore[arg-type]
    )

    with TestClient(app) as client:
        assert client.get("/api/status").json()["editor_available"] is True
        editor = client.get("/cv/")
        dynamic_editor = client.get("/cv/editor/42")

    assert editor.status_code == 200
    assert "Oh My CV" in editor.text
    assert dynamic_editor.status_code == 200
    assert "Editor fallback" in dynamic_editor.text
