"""Regression tests for #698: final-report validator vs canonical schema.

Covers the original #698 lead plus the independent reproduction recorded
during the PR #703 formal review (issue #698 comment 11246):

1. non-dict ``action_log`` entries must fail structured, never crash;
2. the validator must not demand legacy fields the canonical schema forbids
   (``Pinned reviewed head``, ``Scratch worktree used``, ``Worktree path``,
   ``Worktree dirty``, ``Mutations``, ``Next``);
3. a legitimately blocked report (``Candidate head SHA: none``, no formal
   verdict) must not owe approval/merge live-head proofs;
4. canonical reviewer lease release must not be misclassified as post-merge
   cleanup;
5. structured workflow-load and validation proof must be recognized;
6. review mutations are inferred only from authoritative evidence.
"""

from __future__ import annotations

import sys
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

import final_report_validator as frv  # noqa: E402
import post_merge_cleanup_proof as pmcp  # noqa: E402
import pr_work_lease as pwl  # noqa: E402
import review_proofs as rp  # noqa: E402
from review_final_report_schema import (  # noqa: E402
    assess_review_final_report_schema,
)

REVIEWED_HEAD = "a" * 40
LIVE_HEAD = "a" * 40


def _pr703_style_report(
    *,
    decision: str = "request_changes",
    cleanup_mutations: str = (
        "released reviewer PR lease via gitea_release_reviewer_pr_lease "
        "(terminal lease marker phase=released posted)"
    ),
) -> str:
    """Canonical-schema report modeled on the PR #703 formal review handoff."""
    return f"""
Formal review completed with a REQUEST_CHANGES verdict submitted and read
back via the native review API.

## Controller Handoff

- Task: review-merge-pr
- Repo: Scaled-Tech-Consulting/Gitea-Tools
- Role: reviewer
- Identity: sysadmin / prgs-reviewer
- Active profile: prgs-reviewer
- Runtime context: neutral workspace binding
- Selected PR: 703
- Linked issue: #702 open
- Eligibility class: reviewable
- Queue ordering policy: oldest eligible first
- Inventory pagination proof: has_more=false, total_count=8
- Earlier PRs skipped: none
- Candidate head SHA: {REVIEWED_HEAD}
- Reviewed head SHA: {REVIEWED_HEAD}
- Target branch: master
- Target branch SHA: {"2" * 40}
- Already-landed gate: not landed
- Author-safety result: pass (author differs from reviewer)
- Prior request-changes state: none
- Review worktree used: true
- Review worktree path: branches/review-pr-703-independent
- Review worktree inside branches: true
- Review worktree HEAD state: detached at pinned head
- Review worktree dirty before validation: clean
- Review worktree dirty after validation: clean
- Baseline worktree used: false
- Baseline worktree path: none
- Files reviewed: 4
- Validation: focused 50 passed; related 94 passed; full 2665 passed, 6 skipped
- Official validation integrity status: intact
- Terminal review mutation: one REQUEST_CHANGES review submitted and read back
- Review decision: {decision}
- Merge preflight: not run
- Merge result: none
- Linked issue status: open (live fetch proof: gitea_view_issue)
- Main checkout branch: master
- Main checkout dirty state: clean
- Main checkout updated: false
- File edits by reviewer: none
- Worktree/index mutations: none
- Git ref mutations: git fetch prgs (recorded)
- MCP/Gitea mutations: review submission and lease comments only
- Review mutations: one formal REQUEST_CHANGES verdict
- Merge mutations: none
- Cleanup mutations: {cleanup_mutations}
- External-state mutations: none
- Read-only diagnostics: gitea_view_pr, gitea_get_pr_review_feedback
- Blockers: findings F1-F6 recorded on the PR thread
- Current status: review complete; author remediation required
- Safe next action: author addresses findings and pushes a new head
- Safety statement: no merge attempted; no self-review; no root-checkout edits
- Workflow-load helper result: workflow_hash=da045d1e1f1f boundary_status=clean
- Live head SHA before approval: {LIVE_HEAD}
- Pushes occurred during validation: no
"""


def _blocked_preflight_report() -> str:
    """Blocked-run report modeled on the #702 comment 11164 reproduction."""
    return """
Fresh review preflight stopped before any worktree or validation work.

## Controller Handoff

- Task: review-merge-pr
- Repo: Scaled-Tech-Consulting/Gitea-Tools
- Role: reviewer
- Identity: sysadmin / prgs-reviewer
- Active profile: prgs-reviewer
- Runtime context: stale workspace binding detected
- Selected PR: 701
- Linked issue: #699 open
- Eligibility class: blocked-before-validation
- Queue ordering policy: oldest eligible first
- Inventory pagination proof: has_more=false, total_count=8
- Earlier PRs skipped: none
- Candidate head SHA: none
- Reviewed head SHA: none
- Target branch: master
- Target branch SHA: none
- Already-landed gate: not run
- Author-safety result: not run
- Prior request-changes state: none
- Review worktree used: false
- Review worktree path: none
- Review worktree inside branches: not applicable
- Review worktree HEAD state: not applicable
- Review worktree dirty before validation: not applicable
- Review worktree dirty after validation: not applicable
- Baseline worktree used: false
- Baseline worktree path: none
- Files reviewed: 0
- Validation: not run
- Official validation integrity status: not applicable
- Terminal review mutation: none
- Review decision: none
- Merge preflight: not run
- Merge result: none
- Linked issue status: open (live fetch proof: gitea_view_issue)
- Main checkout branch: master
- Main checkout dirty state: clean
- Main checkout updated: false
- File edits by reviewer: none
- Worktree/index mutations: none
- Git ref mutations: none
- MCP/Gitea mutations: none
- Review mutations: none
- Merge mutations: none
- Cleanup mutations: none
- External-state mutations: none
- Read-only diagnostics: gitea_view_pr, gitea_get_runtime_context
- Blockers: runtime bound to a foreign task worktree; mutation prohibited
- Current status: stopped before validation began
- Next actor: operator
- Next action: repair the runtime workspace binding, then rerun the full
  review workflow in a fresh reviewer session
- Next prompt: Act as REVIEWER for PR 701 after the operator repairs the
  runtime binding; acquire the lease before any validation.
- Safe next action: operator repairs runtime binding, then a fresh reviewer
  reruns the full workflow
- Safety statement: no lease acquired; no verdict recorded; no source edits
- Workflow-load helper result: workflow_hash=da045d1e1f1f boundary_status=clean
"""


class TestActionLogRobustness(unittest.TestCase):
    """#698 original lead: non-dict action_log must not crash validation."""

    MALFORMED = [
        "git fetch prgs",
        42,
        None,
        {"action": "edit", "path": "x.py", "performed": True, "tracked": True},
    ]

    def test_assess_final_report_validator_survives_malformed_entries(self):
        result = frv.assess_final_report_validator(
            _pr703_style_report(),
            "review_pr",
            action_log=self.MALFORMED,
        )
        self.assertIsInstance(result, dict)
        rule_ids = {f["rule_id"] for f in result["findings"]}
        self.assertIn("shared.action_log_malformed", rule_ids)

    def test_malformed_entry_errors_are_sanitized(self):
        _entries, findings = frv.sanitize_action_log(["secret-token-abc123"])
        self.assertEqual(len(findings), 1)
        reason = findings[0]["reason"]
        self.assertNotIn("secret-token-abc123", reason)
        self.assertIn("str", reason)
        self.assertIn("entry 0", reason)

    def test_non_list_action_log_is_reported_not_raised(self):
        entries, findings = frv.sanitize_action_log("not-a-list")
        self.assertEqual(entries, [])
        self.assertEqual(len(findings), 1)
        self.assertIn("not a list", findings[0]["reason"])

    def test_performed_file_mutations_skips_non_dict_entries(self):
        performed = rp._performed_file_mutations(
            ["oops", {"action": "edited", "path": "a.py"}]
        )
        self.assertEqual(len(performed), 1)
        self.assertEqual(performed[0]["path"], "a.py")

    def test_schema_entrypoint_survives_string_only_log(self):
        result = assess_review_final_report_schema(
            _pr703_style_report(),
            action_log=["just a string", "another string"],
        )
        self.assertIsInstance(result, dict)


class TestLegacyFieldRequirementsRemoved(unittest.TestCase):
    """#698: prohibited legacy fields must not be REQUIRED of reports."""

    PROHIBITED = (
        "Pinned reviewed head",
        "Scratch worktree used",
        "Worktree path",
        "Worktree dirty",
        "Workspace mutations",
        "Mutations",
        "Next",
        "Issue/PR",
        "Branch/SHA",
        "Files changed",
    )

    def test_review_role_field_table_has_no_prohibited_requirements(self):
        names = [name for name, _ in rp.HANDOFF_ROLE_FIELDS["review"]]
        for prohibited in ("Pinned reviewed head", "Scratch worktree used",
                           "Worktree path", "Worktree dirty"):
            self.assertNotIn(prohibited, names)

    def test_merger_role_field_table_has_no_pinned_reviewed_head(self):
        names = [name for name, _ in rp.HANDOFF_ROLE_FIELDS["merger"]]
        self.assertNotIn("Pinned reviewed head", names)

    def test_canonical_report_missing_fields_never_include_prohibited(self):
        result = rp.assess_controller_handoff(
            _pr703_style_report(), role="review"
        )
        for prohibited in self.PROHIBITED:
            self.assertNotIn(prohibited, result.get("missing_fields") or [])

    def test_canonical_pr703_report_satisfies_required_fields(self):
        result = rp.assess_controller_handoff(
            _pr703_style_report(), role="review"
        )
        self.assertEqual(result.get("missing_fields") or [], [])
        self.assertEqual(result.get("verdict"), "complete")


class TestBlockedReportAccepted(unittest.TestCase):
    """#698: blocked run with no reviewed head / verdict is legitimate."""

    def test_stale_head_proof_waived_before_validation(self):
        result = pwl.assess_reviewer_stale_head_final_report(
            _blocked_preflight_report()
        )
        self.assertTrue(result["proven"])
        self.assertEqual(result.get("phase"), "blocked_before_validation")

    def test_blocked_report_passes_schema_validation(self):
        result = assess_review_final_report_schema(_blocked_preflight_report())
        blocking = [
            f for f in result["findings"] if f["severity"] == "block"
        ]
        self.assertEqual(blocking, [], blocking)

    def test_verdict_phase_still_demands_approval_head_proof(self):
        report = _blocked_preflight_report().replace(
            "- Review decision: none",
            "- Review decision: approve",
        ).replace(
            "- Candidate head SHA: none",
            f"- Candidate head SHA: {REVIEWED_HEAD}",
        )
        result = pwl.assess_reviewer_stale_head_final_report(report)
        self.assertFalse(result["proven"])
        joined = " ".join(result["reasons"])
        self.assertIn("before approval", joined)

    def test_merge_phase_still_demands_merge_head_proof(self):
        report = _pr703_style_report().replace(
            "- Merge result: none",
            "- Merge result: merged",
        )
        result = pwl.assess_reviewer_stale_head_final_report(report)
        self.assertFalse(result["proven"])
        self.assertIn(
            "final live head SHA before merge not stated",
            result["reasons"],
        )

    def test_validation_phase_demands_push_disclosure(self):
        report = _pr703_style_report().replace(
            "- Pushes occurred during validation: no\n", ""
        )
        result = pwl.assess_reviewer_stale_head_final_report(report)
        self.assertFalse(result["proven"])
        self.assertIn(
            "whether push occurred during validation not stated",
            result["reasons"],
        )


class TestLeaseReleaseVsPostMergeCleanup(unittest.TestCase):
    """#698 (PR #703 review reproduction): lease release is not cleanup."""

    def test_lease_release_cleanup_mutations_do_not_demand_checklist(self):
        result = pmcp.assess_post_merge_cleanup_proof(_pr703_style_report())
        self.assertFalse(result["block"], result["reasons"])

    def test_release_tool_name_alone_is_recognized(self):
        report = _pr703_style_report(
            cleanup_mutations="gitea_release_reviewer_pr_lease comment 11244"
        )
        result = pmcp.assess_post_merge_cleanup_proof(report)
        self.assertFalse(result["block"], result["reasons"])

    def test_substantive_non_lease_cleanup_still_demands_checklist(self):
        report = _pr703_style_report(
            cleanup_mutations="deleted stale scratch directory manually"
        )
        result = pmcp.assess_post_merge_cleanup_proof(report)
        self.assertTrue(result["block"])

    def test_remote_branch_delete_claims_still_demand_full_proof(self):
        report = _pr703_style_report(
            cleanup_mutations="gitea_delete_branch removed the remote branch"
        )
        result = pmcp.assess_post_merge_cleanup_proof(report)
        self.assertTrue(result["block"])
        self.assertTrue(
            any("remote branch deletion missing" in r for r in result["reasons"])
        )

    def test_full_schema_run_accepts_lease_release_report(self):
        result = assess_review_final_report_schema(_pr703_style_report())
        lease_cleanup_blocks = [
            f for f in result["findings"]
            if f["rule_id"] == "reviewer.post_merge_cleanup_proof"
        ]
        self.assertEqual(lease_cleanup_blocks, [], lease_cleanup_blocks)


class TestStructuredProofRecognition(unittest.TestCase):
    """#698: structured workflow-load and validation proof must be accepted."""

    def test_key_value_workflow_proof_recognized(self):
        findings = frv._rule_reviewer_workflow_load_boundary(
            _pr703_style_report()
        )
        self.assertEqual(findings, [], findings)

    def test_colon_form_workflow_proof_still_recognized(self):
        report = _pr703_style_report().replace(
            "- Workflow-load helper result: workflow_hash=da045d1e1f1f "
            "boundary_status=clean",
            "- Workflow-load helper result: workflow_hash: da045d1e1f1f, "
            "boundary_status: clean",
        )
        findings = frv._rule_reviewer_workflow_load_boundary(report)
        self.assertEqual(findings, [], findings)

    def test_incomplete_structured_proof_still_blocks(self):
        report = _pr703_style_report().replace(
            "workflow_hash=da045d1e1f1f boundary_status=clean",
            "workflow_hash=da045d1e1f1f",
        )
        findings = frv._rule_reviewer_workflow_load_boundary(report)
        self.assertTrue(findings)
        self.assertIn("boundary_status", findings[0]["reason"])

    def test_validation_counts_accepted_as_pass_proof(self):
        # "Validation: focused 50 passed; ..." must satisfy the reviewed-head
        # validation-proof rule (PR #703 reproduction).
        result = assess_review_final_report_schema(_pr703_style_report())
        head_blocks = [
            f for f in result["findings"]
            if f["rule_id"] == "reviewer.reviewed_head_without_validation"
        ]
        self.assertEqual(head_blocks, [], head_blocks)


class TestAuthoritativeMutationInference(unittest.TestCase):
    """#698: review mutations inferred only from authoritative evidence."""

    def test_read_only_entries_do_not_imply_mutations(self):
        report = "## Controller Handoff\n- Mutations: none\n"
        findings = frv._rule_reviewer_vague_mutations_none(
            report,
            action_log=[
                {"action": "gitea_view_pr"},
                {"action": "gitea_get_pr_review_feedback", "performed": False},
            ],
        )
        self.assertEqual(findings, [], findings)

    def test_performed_mutation_still_blocks_vague_none(self):
        report = "## Controller Handoff\n- Mutations: none\n"
        findings = frv._rule_reviewer_vague_mutations_none(
            report,
            action_log=[{"action": "edit", "path": "a.py", "performed": True}],
        )
        self.assertTrue(findings)

    def test_gated_rejection_is_not_a_mutation(self):
        report = "## Controller Handoff\n- Mutations: none\n"
        findings = frv._rule_reviewer_vague_mutations_none(
            report,
            action_log=[
                {"action": "edit", "path": "a.py", "performed": True,
                 "gated_rejected": True},
            ],
        )
        self.assertEqual(findings, [], findings)


class TestValidatorRuleErrorContainment(unittest.TestCase):
    """#698: a defective rule fails closed with a sanitized error."""

    def test_rule_exception_becomes_sanitized_block_finding(self):
        def _boom(report_text):
            raise ValueError("raw secret detail that must not leak")

        original = frv._RULES_BY_TASK["review_pr"]
        frv._RULES_BY_TASK["review_pr"] = [_boom]
        try:
            result = frv.assess_final_report_validator(
                "report body", "review_pr"
            )
        finally:
            frv._RULES_BY_TASK["review_pr"] = original
        self.assertTrue(result["blocked"])
        finding = next(
            f for f in result["findings"]
            if f["rule_id"] == "shared.validator_rule_error"
        )
        self.assertNotIn("raw secret detail", finding["reason"])
        self.assertIn("ValueError", finding["reason"])
        self.assertIn("_boom", finding["reason"])


if __name__ == "__main__":
    unittest.main()
