# grantflow/tests/test_integration_critic_findings.py

from fastapi.testclient import TestClient

from grantflow.api import runtime_state
from grantflow.api.app import app

client = TestClient(app)


def test_status_critic_findings_can_be_acknowledged_resolved_and_linked_to_comments():
    job_id = "critic-findings-linkage-1"
    runtime_state.JOB_STORE.set(
        job_id,
        {
            "status": "done",
            "state": {
                "quality_score": 6.5,
                "critic_score": 6.5,
                "needs_revision": True,
                "critic_notes": {
                    "engine": "rules",
                    "rule_score": 6.5,
                    "llm_score": None,
                    "llm_advisory_diagnostics": {
                        "llm_finding_count": 1,
                        "candidate_label_counts": {"TOY_LABEL": 1},
                        "advisory_candidate_count": 0,
                        "advisory_candidate_labels": [],
                        "advisory_applies": False,
                        "advisory_rejected_reason": "non_advisory_llm_finding_present",
                    },
                    "revision_instructions": "Fix ToC and indicators.",
                    "fatal_flaws": [
                        {
                            "code": "TOC_SCHEMA_INVALID",
                            "severity": "high",
                            "section": "general",
                            "message": "Theory of Change draft does not match expected donor schema.",
                            "fix_hint": "Regenerate ToC with valid structured output.",
                            "source": "rules",
                        }
                    ],
                    "fatal_flaw_messages": ["Theory of Change draft does not match expected donor schema."],
                    "rule_checks": [{"code": "TOC_SCHEMA_VALID", "status": "fail", "section": "toc"}],
                },
            },
            "review_comments": [],
        },
    )

    critic_resp = client.get(f"/status/{job_id}/critic")
    assert critic_resp.status_code == 200
    critic_body = critic_resp.json()
    assert critic_body["fatal_flaws"]
    assert critic_body["llm_advisory_diagnostics"]["llm_finding_count"] == 1
    assert critic_body["llm_advisory_diagnostics"]["advisory_rejected_reason"] == "non_advisory_llm_finding_present"
    finding = critic_body["fatal_flaws"][0]
    finding_id = finding["finding_id"]
    assert isinstance(finding.get("due_at"), str) and finding["due_at"]
    assert int(finding.get("sla_hours") or 0) == 24

    ack_resp = client.post(
        f"/status/{job_id}/critic/findings/{finding_id}/ack",
        headers={"X-Reviewer": "qa_reviewer"},
    )
    assert ack_resp.status_code == 200
    ack_body = ack_resp.json()
    assert ack_body["id"] == finding_id
    assert ack_body["finding_id"] == finding_id
    assert ack_body["status"] == "acknowledged"
    assert ack_body.get("acknowledged_at")
    assert ack_body.get("updated_at")
    assert ack_body.get("updated_by") == "qa_reviewer"
    assert ack_body.get("acknowledged_by") == "qa_reviewer"
    assert isinstance(ack_body.get("due_at"), str) and ack_body["due_at"]

    comment_resp = client.post(
        f"/status/{job_id}/comments",
        json={
            "section": ack_body["section"],
            "message": "Acknowledged by reviewer; will revise next iteration.",
            "linked_finding_id": finding_id,
        },
    )
    assert comment_resp.status_code == 200
    comment_body = comment_resp.json()
    assert comment_body["linked_finding_id"] == finding_id
    assert int(comment_body.get("sla_hours") or 0) == 24
    assert isinstance(comment_body.get("due_at"), str) and comment_body["due_at"]

    critic_resp_2 = client.get(f"/status/{job_id}/critic")
    assert critic_resp_2.status_code == 200
    critic_body_2 = critic_resp_2.json()
    linked_finding = next(f for f in critic_body_2["fatal_flaws"] if f["finding_id"] == finding_id)
    assert comment_body["comment_id"] in (linked_finding.get("linked_comment_ids") or [])

    resolve_resp = client.post(
        f"/status/{job_id}/critic/findings/{finding_id}/resolve",
        headers={"X-User": "qa_resolver"},
    )
    assert resolve_resp.status_code == 200
    resolve_body = resolve_resp.json()
    assert resolve_body["id"] == finding_id
    assert resolve_body["finding_id"] == finding_id
    assert resolve_body["status"] == "resolved"
    assert resolve_body.get("resolved_at")
    assert resolve_body.get("updated_at")
    assert resolve_body.get("updated_by") == "qa_resolver"
    assert resolve_body.get("acknowledged_by") == "qa_reviewer"
    assert resolve_body.get("resolved_by") == "qa_resolver"

    reopen_resp = client.post(
        f"/status/{job_id}/critic/findings/{finding_id}/open",
        headers={"X-Actor": "qa_reopener"},
    )
    assert reopen_resp.status_code == 200
    reopen_body = reopen_resp.json()
    assert reopen_body["id"] == finding_id
    assert reopen_body["finding_id"] == finding_id
    assert reopen_body["status"] == "open"
    assert reopen_body.get("updated_at")
    assert reopen_body.get("updated_by") == "qa_reopener"
    assert reopen_body.get("acknowledged_at") is None
    assert reopen_body.get("acknowledged_by") is None
    assert reopen_body.get("resolved_at") is None
    assert reopen_body.get("resolved_by") is None
    assert isinstance(reopen_body.get("due_at"), str) and reopen_body["due_at"]
    assert reopen_body["due_at"] != ack_body["due_at"]


def test_status_critic_normalizes_legacy_string_findings_into_entities():
    job_id = "critic-findings-legacy-string-1"
    runtime_state.JOB_STORE.set(
        job_id,
        {
            "status": "done",
            "state": {
                "quality_score": 5.0,
                "critic_score": 5.0,
                "needs_revision": True,
                "critic_notes": {
                    "engine": "rules",
                    "fatal_flaws": [
                        "Missing indicator baseline and target for key outcome.",
                    ],
                    "fatal_flaw_messages": ["Missing indicator baseline and target for key outcome."],
                    "rule_checks": [],
                },
            },
        },
    )

    critic_resp = client.get(f"/status/{job_id}/critic")
    assert critic_resp.status_code == 200
    critic_body = critic_resp.json()
    assert critic_body["fatal_flaw_count"] == 1
    flaw = critic_body["fatal_flaws"][0]
    assert flaw["id"] == flaw["finding_id"]
    assert flaw["finding_id"]
    assert flaw["code"] == "LEGACY_UNSTRUCTURED_FINDING"
    assert flaw["status"] == "open"
    assert flaw["source"] == "rules"
    assert flaw["message"].startswith("Missing indicator baseline")
    assert flaw.get("fix_suggestion")

    ack_resp = client.post(f"/status/{job_id}/critic/findings/{flaw['finding_id']}/ack")
    assert ack_resp.status_code == 200
    ack_body = ack_resp.json()
    assert ack_body["status"] == "acknowledged"
    assert ack_body.get("acknowledged_at")


def test_status_critic_accepts_id_only_finding_entities():
    job_id = "critic-findings-id-only-1"
    runtime_state.JOB_STORE.set(
        job_id,
        {
            "status": "done",
            "state": {
                "quality_score": 6.0,
                "critic_score": 6.0,
                "needs_revision": True,
                "critic_notes": {
                    "engine": "rules",
                    "fatal_flaws": [
                        {
                            "id": "finding-id-only-1",
                            "code": "TOC_SCHEMA_INVALID",
                            "severity": "high",
                            "section": "toc",
                            "status": "open",
                            "message": "Theory of Change draft does not match expected donor schema.",
                            "fix_suggestion": "Regenerate ToC with valid structured output.",
                            "source": "rules",
                        }
                    ],
                    "fatal_flaw_messages": ["Theory of Change draft does not match expected donor schema."],
                    "rule_checks": [{"code": "TOC_SCHEMA_VALID", "status": "fail", "section": "toc"}],
                },
            },
            "review_comments": [],
        },
    )

    critic_resp = client.get(f"/status/{job_id}/critic")
    assert critic_resp.status_code == 200
    flaw = critic_resp.json()["fatal_flaws"][0]
    assert flaw["id"] == "finding-id-only-1"
    assert flaw["finding_id"] == "finding-id-only-1"

    ack_resp = client.post(f"/status/{job_id}/critic/findings/{flaw['id']}/ack")
    assert ack_resp.status_code == 200
    ack_body = ack_resp.json()
    assert ack_body["id"] == "finding-id-only-1"
    assert ack_body["finding_id"] == "finding-id-only-1"
    assert ack_body["status"] == "acknowledged"
