import threading

import pytest
from fastapi.testclient import TestClient

import backend.jobs as jobs
from backend.main import app


class _SyncThread:
    """Drop-in threading.Thread replacement that runs the target inline."""

    def __init__(self, target=None, args=(), kwargs=None, **_ignored):
        self._target = target
        self._args = args
        self._kwargs = kwargs or {}

    def start(self):
        self._target(*self._args, **self._kwargs)


@pytest.fixture
def job_env(monkeypatch, tmp_path):
    monkeypatch.setattr("backend.config.OUTPUT_DIR", str(tmp_path))
    monkeypatch.setattr(threading, "Thread", _SyncThread)
    monkeypatch.setattr("backend.jobs.send_conflict_report", lambda *a, **k: True)
    pdf = tmp_path / "set.pdf"
    pdf.write_bytes(b"%PDF-1.4\n")
    yield tmp_path
    jobs._jobs.clear()


def test_job_log_captures_pipeline_output(job_env, monkeypatch):
    def fake_runner(pdf_path, **kwargs):
        print("STAGE banner: fake wave ran")
        return {"source": "set.pdf", "summary": {"conflicts_found": 0}}

    monkeypatch.setattr("backend.jobs.run_pipeline", fake_runner)
    job_id = jobs.create_job(str(job_env / "set.pdf"), "set.pdf", pipeline_mode="classic")

    log_path = job_env / job_id / "job.log"
    assert log_path.is_file()
    content = log_path.read_text()
    assert "STAGE banner: fake wave ran" in content
    assert job_id in content  # header line


def test_job_log_endpoint_serves_log_and_404s(job_env, monkeypatch):
    monkeypatch.setattr(
        "backend.jobs.run_pipeline",
        lambda pdf_path, **kw: {"source": "s", "summary": {}},
    )
    job_id = jobs.create_job(str(job_env / "set.pdf"), "set.pdf", pipeline_mode="classic")

    client = TestClient(app)
    ok = client.get(f"/jobs/{job_id}/log")
    assert ok.status_code == 200
    assert ok.headers["content-type"].startswith("text/plain")
    assert "Job " + job_id in ok.text
    assert client.get("/jobs/nope/log").status_code == 404


def test_model_overrides_passed_to_classic_runner(job_env, monkeypatch):
    """Classic mode: per-run picks travel as run_pipeline kwargs (the runner
    sets and clears llm.set_model_overrides itself)."""
    seen = {}

    def fake_runner(pdf_path, **kwargs):
        seen.update(kwargs)
        return {"source": "set.pdf", "summary": {}}

    monkeypatch.setattr("backend.jobs.run_pipeline", fake_runner)
    jobs.create_job(str(job_env / "set.pdf"), "set.pdf",
                    pipeline_mode="classic",
                    vision_model="openai/gpt-4o", text_model="openai/gpt-4o-mini")

    assert seen["vision_model"] == "openai/gpt-4o"
    assert seen["text_model"] == "openai/gpt-4o-mini"


def test_model_overrides_set_and_cleared_around_agent_run(job_env, monkeypatch):
    """Agent mode: the agent runner has no override params, so jobs.py sets
    them module-level for the duration of the run."""
    from backend import llm

    seen = {}

    def fake_agent_runner(pdf_path, **kwargs):
        seen["vision"] = llm._vision_model_override
        seen["text"] = llm._text_model_override
        return {"source": "set.pdf", "summary": {}}

    monkeypatch.setattr("backend.jobs.run_agent_pipeline", fake_agent_runner)
    jobs.create_job(str(job_env / "set.pdf"), "set.pdf",
                    pipeline_mode="agent",
                    vision_model="openai/gpt-4o", text_model="openai/gpt-4o-mini")

    assert seen["vision"] == "openai/gpt-4o"
    assert seen["text"] == "openai/gpt-4o-mini"
    assert llm._vision_model_override is None  # cleared after the run
    assert llm._text_model_override is None


def test_failed_run_logs_traceback(job_env, monkeypatch):
    """A crashed job must leave the traceback in job.log, not just str(e)."""
    def boom(pdf_path, **kwargs):
        raise RuntimeError("kaboom-stage-failure")

    monkeypatch.setattr("backend.jobs.run_pipeline", boom)
    job_id = jobs.create_job(str(job_env / "set.pdf"), "set.pdf", pipeline_mode="classic")

    assert jobs._jobs[job_id]["status"] == "error"
    content = (job_env / job_id / "job.log").read_text()
    assert "Traceback (most recent call last)" in content
    assert "RuntimeError: kaboom-stage-failure" in content
