from __future__ import annotations

from uuid import UUID

import pytest
from fastapi import HTTPException

from src.api.v1.platform.events import ChannelEventRecurrenceResponse
from src.services import channel_event_dossier_service
from src.services.channel_event_dossier_service import (
    RecurrenceWorkItemNotFoundError,
    build_dossier_coverage,
    build_dossier_event,
    build_dossier_recurrence,
    build_recurrence_work_item_dry_run,
    build_recurrence_work_item_handoff,
    build_recurrence_work_item_preview,
    build_source_correlation_apply,
    build_source_correlation_review_decision,
    fetch_channel_event_dossier,
    fetch_channel_event_dossier_coverage,
    fetch_channel_event_dossier_recurrence,
)


def test_build_dossier_event_summarizes_source_envelope() -> None:
    event = build_dossier_event(
        {
            "event_id": "event-1",
            "project_id": "awoooi",
            "channel_type": "internal",
            "provider_event_id": "sentry:received:issue-1",
            "content_hash": "h" * 64,
            "content_preview": "Sentry issue",
            "content_redacted": "Sentry issue redacted",
            "redaction_version": "audit_sink_v1",
            "source_envelope": {
                "provider": "sentry",
                "stage": "received",
                "source_url": "https://sentry.example.invalid/issues/issue-1",
                "content_sha256": "a" * 64,
                "content_length": 42,
                "source_refs": {
                    "event_ids": ["issue-1"],
                    "sentry_issue_ids": ["issue-1", "sentry:received:issue-1"],
                    "fingerprints": ["sentry-issue-1"],
                },
                "log_correlation": {
                    "alertname": "Sentry Issue",
                    "severity": "error",
                    "namespace": "sentry",
                    "target_resource": "frontend",
                    "fingerprint": "sentry-issue-1",
                },
            },
            "is_duplicate": False,
            "provider_ts": None,
            "received_at": "2026-05-13T13:46:00",
        }
    )

    assert event["provider"] == "sentry"
    assert event["stage"] == "received"
    assert event["alertname"] == "Sentry Issue"
    assert event["severity"] == "error"
    assert event["source_ref_count"] == 4
    assert event["has_redacted_content"] is True
    assert event["content_sha256"] == "a" * 64


def test_build_dossier_coverage_summarizes_recent_sources() -> None:
    coverage = build_dossier_coverage(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "sentry:received:issue-1",
                "content_hash": "h" * 64,
                "content_preview": "Sentry issue",
                "content_redacted": "Sentry issue redacted",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "sentry",
                    "stage": "received",
                    "source_refs": {
                        "sentry_issue_ids": ["issue-1"],
                        "alert_ids": ["sentry:received:issue-1"],
                        "fingerprints": ["fingerprint-1"],
                    },
                },
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-13T13:46:00",
            },
            {
                "event_id": "event-2",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "signoz:received:alert-1",
                "content_hash": "i" * 64,
                "content_preview": "SignOz alert",
                "content_redacted": None,
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "signoz",
                    "stage": "received",
                    "source_refs": {
                        "signoz_alerts": ["alert-1"],
                        "alert_ids": ["signoz:received:alert-1"],
                    },
                },
                "is_duplicate": True,
                "provider_ts": None,
                "received_at": "2026-05-13T13:45:00",
            },
            {
                "event_id": "event-3",
                "project_id": "awoooi",
                "channel_type": "telegram",
                "provider_event_id": "telegram:callback:1",
                "content_hash": None,
                "content_preview": "Callback",
                "content_redacted": None,
                "redaction_version": "audit_sink_v1",
                "source_envelope": {},
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-13T13:44:00",
            },
        ],
        project_id="awoooi",
        limit=100,
    )

    assert coverage["project_id"] == "awoooi"
    assert coverage["summary"]["source_count"] == 3
    assert coverage["summary"]["source_envelope_total"] == 2
    assert coverage["summary"]["missing_source_envelope_total"] == 1
    assert coverage["summary"]["with_source_refs_total"] == 2
    assert coverage["summary"]["missing_source_refs_total"] == 1
    assert coverage["summary"]["duplicate_total"] == 1
    assert coverage["summary"]["redacted_total"] == 1
    assert coverage["summary"]["sentry_ref_total"] == 1
    assert coverage["summary"]["signoz_ref_total"] == 1
    assert coverage["summary"]["alert_ref_total"] == 2
    assert coverage["providers"][0]["provider"] == "sentry"


def test_build_dossier_recurrence_groups_events_and_run_state() -> None:
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-2",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "alertmanager:received:2",
                "content_hash": "b" * 64,
                "content_preview": "Host disk pressure",
                "content_redacted": "Host disk pressure",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "alertmanager",
                    "source_refs": {
                        "alert_ids": ["alert-2"],
                        "incident_ids": ["INC-20260513-ABCD"],
                        "fingerprints": ["fp-host-disk"],
                    },
                    "log_correlation": {
                        "alertname": "HostDiskUsageHigh",
                        "severity": "warning",
                        "namespace": "node",
                        "target_resource": "host-110",
                        "fingerprint": "fp-host-disk",
                    },
                },
                "is_duplicate": True,
                "provider_ts": None,
                "received_at": "2026-05-13T13:47:00",
                "run_id": UUID("11111111-1111-4111-8111-111111111111"),
                "run_state": "waiting_approval",
                "run_agent_id": "openclaw",
            },
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "alertmanager:received:1",
                "content_hash": "a" * 64,
                "content_preview": "Host disk pressure",
                "content_redacted": "Host disk pressure",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "alertmanager",
                    "source_refs": {
                        "alert_ids": ["alert-1"],
                        "incident_ids": ["INC-20260513-ABCD"],
                        "fingerprints": ["fp-host-disk"],
                    },
                    "log_correlation": {
                        "alertname": "HostDiskUsageHigh",
                        "severity": "warning",
                        "namespace": "node",
                        "target_resource": "host-110",
                        "fingerprint": "fp-host-disk",
                    },
                },
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-13T13:46:00",
                "run_id": UUID("22222222-2222-4222-8222-222222222222"),
                "run_state": "completed",
                "run_agent_id": "openclaw",
            },
            {
                "event_id": "event-3",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "sentry:received:issue-1",
                "content_hash": "c" * 64,
                "content_preview": "Sentry issue",
                "content_redacted": "Sentry issue",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "sentry",
                    "source_refs": {"sentry_issue_ids": ["issue-1"]},
                    "log_correlation": {"alertname": "Sentry Issue"},
                },
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-13T13:45:00",
                "run_id": None,
                "run_state": None,
                "run_agent_id": None,
            },
        ],
        project_id="awoooi",
        limit=100,
        repair_summaries_by_incident={
            "INC-20260513-ABCD": {
                "schema_version": "awooop_recurrence_repair_summary_v1",
                "incident_id": "INC-20260513-ABCD",
                "latest_auto_repair_id": "repair-1",
                "latest_playbook_id": "playbook-1",
                "latest_playbook_name": "Restart workload",
                "latest_success": False,
                "latest_error_message_preview": "verifier failed",
                "latest_triggered_by": "auto_repair",
                "latest_risk_level": "low",
                "latest_execution_time_ms": 1200,
                "latest_auto_repair_at": "2026-05-13T13:48:00",
                "latest_verification_result": "failed",
                "latest_verification_at": "2026-05-13T13:49:00",
                "auto_repair_total": 1,
                "success_total": 0,
                "failed_total": 1,
            },
        },
    )

    assert recurrence["summary"]["source_event_total"] == 3
    assert recurrence["summary"]["recurrence_group_total"] == 2
    assert recurrence["summary"]["recurrent_group_total"] == 1
    assert recurrence["summary"]["duplicate_event_total"] == 1
    assert recurrence["summary"]["linked_run_total"] == 2
    assert recurrence["summary"]["unlinked_event_total"] == 1
    assert recurrence["summary"]["auto_repair_linked_total"] == 1
    assert recurrence["summary"]["open_work_item_group_total"] == 2
    assert recurrence["summary"]["verified_repair_group_total"] == 0
    assert recurrence["summary"]["automation_gap_group_total"] == 0
    assert recurrence["summary"]["failed_repair_group_total"] == 1
    assert recurrence["summary"]["source_correlation_review_group_total"] == 1
    assert recurrence["summary"]["source_correlation_decision_recorded_group_total"] == 0

    host_group = recurrence["items"][0]
    assert host_group["recurrence_key"] == "fingerprint:fp-host-disk"
    assert host_group["occurrence_total"] == 2
    assert host_group["duplicate_total"] == 1
    assert host_group["linked_run_total"] == 2
    assert host_group["latest_run_state"] == "waiting_approval"
    assert host_group["latest_incident_id"] == "INC-20260513-ABCD"
    assert host_group["incident_ids"] == ["INC-20260513-ABCD"]
    assert host_group["latest_stage"] == "received"
    assert host_group["stage_counts"] == {"received": 2}
    assert host_group["run_state_counts"] == {"waiting_approval": 1, "completed": 1}
    assert host_group["alert_ref_total"] == 2
    assert host_group["repair_summary"]["status"] == "auto_repair_failed"
    assert host_group["repair_summary"]["latest_auto_repair_id"] == "repair-1"
    assert host_group["work_item"] == {
        "schema_version": "awooop_recurrence_work_item_link_v1",
        "work_item_id": "verification:INC-20260513-ABCD:repair-1",
        "incident_id": "INC-20260513-ABCD",
        "matched_incident_id": None,
        "auto_repair_id": "repair-1",
        "status": "open",
        "kind": "verification",
        "next_step": "triage_failed_repair",
        "reason": "auto_repair_failed",
        "needs_human": True,
    }

    source_group = recurrence["items"][1]
    assert source_group["provider"] == "sentry"
    assert source_group["latest_stage"] == "received"
    assert source_group["stage_counts"] == {"received": 1}
    assert source_group["repair_summary"]["status"] == "source_correlation_review"
    assert source_group["work_item"] == {
        "schema_version": "awooop_recurrence_work_item_link_v1",
        "work_item_id": "source-evidence:sentry:received:issue-1",
        "incident_id": None,
        "matched_incident_id": None,
        "auto_repair_id": None,
        "status": "open",
        "kind": "source_correlation_review",
        "next_step": "review_provider_source_match",
        "reason": "provider_native_evidence_unlinked",
        "needs_human": True,
    }


def test_build_recurrence_work_item_preview_allows_source_correlation_review() -> None:
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "signoz:upstream_canary:canary-1",
                "content_hash": "a" * 64,
                "content_preview": "SignOz upstream canary",
                "content_redacted": "SignOz upstream canary",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "signoz",
                    "stage": "upstream_canary",
                    "source_refs": {
                        "signoz_alerts": ["alert-1"],
                        "alert_ids": ["signoz:upstream_canary:canary-1"],
                    },
                    "log_correlation": {
                        "alertname": "Source Provider Upstream Canary",
                        "severity": "info",
                        "namespace": "observability",
                        "target_resource": "signoz",
                        "fingerprint": "fp-signoz-canary",
                    },
                },
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-20T13:01:00",
                "run_id": None,
                "run_state": None,
                "run_agent_id": None,
            }
        ],
        project_id="awoooi",
        limit=20,
    )

    item = recurrence["items"][0]
    work_item_id = "source-evidence:signoz:upstream_canary:canary-1"
    assert recurrence["summary"]["source_correlation_review_group_total"] == 1
    assert recurrence["summary"]["source_correlation_decision_recorded_group_total"] == 0
    assert recurrence["summary"]["open_work_item_group_total"] == 1
    assert item["latest_stage"] == "upstream_canary"
    assert item["repair_summary"]["status"] == "source_correlation_review"
    assert item["work_item"]["work_item_id"] == work_item_id
    assert item["work_item"]["kind"] == "source_correlation_review"
    assert item["work_item"]["next_step"] == "review_provider_source_match"

    preview = build_recurrence_work_item_preview(
        recurrence,
        work_item_id=work_item_id,
    )

    assert preview["mode"] == "observe"
    assert preview["allowed"] is True
    assert preview["plan"]["target_action"] == "review_provider_source_match"
    assert preview["plan"]["target"]["latest_stage"] == "upstream_canary"

    dry_run = build_recurrence_work_item_dry_run(
        recurrence,
        work_item_id=work_item_id,
    )

    assert dry_run["verification_result_preview"] == "observe_only"
    assert dry_run["ticket_preview"]["would_create"] is False
    assert "Source evidence review" in dry_run["ticket_preview"]["title"]
    assert dry_run["current_state_summary"]["latest_stage"] == "upstream_canary"
    assert dry_run["current_state_summary"]["latest_provider_event_id"] == (
        "signoz:upstream_canary:canary-1"
    )


def test_source_correlation_review_decision_records_accepted_audit_contract() -> None:
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "sentry:received:issue-1",
                "content_hash": "a" * 64,
                "content_preview": "Sentry issue",
                "content_redacted": "Sentry issue",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "sentry",
                    "stage": "received",
                    "source_refs": {
                        "sentry_issue_ids": ["issue-1"],
                        "alert_ids": ["sentry:received:issue-1"],
                    },
                    "log_correlation": {
                        "alertname": "Sentry Issue",
                        "severity": "error",
                        "namespace": "awoooi-prod",
                        "target_resource": "web",
                        "fingerprint": "fp-sentry-issue-1",
                    },
                },
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-20T13:10:00",
                "run_id": None,
                "run_state": None,
                "run_agent_id": None,
            }
        ],
        project_id="awoooi",
        limit=20,
    )

    decision = build_source_correlation_review_decision(
        recurrence,
        work_item_id="source-evidence:sentry:received:issue-1",
        decision="accepted",
        target_incident_id="INC-20260520-ABC123",
        reviewer_id="operator_console",
        operator_note="matches current frontend incident",
    )

    assert decision["schema_version"] == "awooop_source_correlation_review_decision_v1"
    assert decision["allowed"] is True
    assert decision["executed"] is True
    assert decision["decision"] == "accepted"
    assert decision["review_status"] == "accepted"
    assert decision["target_incident_id"] == "INC-20260520-ABC123"
    assert decision["writes_incident_state"] is False
    assert decision["writes_source_event"] is False
    assert decision["writes_auto_repair_result"] is False
    assert decision["writes_ticket"] is False
    assert decision["creates_external_ticket"] is False
    assert decision["plan"]["step"] == "record_source_correlation_review_decision"
    assert decision["plan"]["writes"] == ["alert_operation_log", "timeline_events"]
    assert decision["next_step"] == "verify_source_match_in_status_chain"


def test_build_dossier_recurrence_closes_source_review_after_decision() -> None:
    decision = {
        "schema_version": "awooop_source_correlation_review_decision_v1",
        "review_id": "review-1",
        "work_item_id": "source-evidence:sentry:received:issue-1",
        "decision": "accepted",
        "review_status": "accepted",
        "target_incident_id": "INC-20260520-ABC123",
        "reviewer_id": "operator_console",
        "latest_provider_event_id": "sentry:received:issue-1",
        "recorded_at": "2026-05-20T13:11:00",
    }
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "sentry:received:issue-1",
                "content_hash": "a" * 64,
                "content_preview": "Sentry issue",
                "content_redacted": "Sentry issue",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "sentry",
                    "stage": "received",
                    "source_refs": {"sentry_issue_ids": ["issue-1"]},
                },
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-20T13:10:00",
                "run_id": None,
                "run_state": None,
                "run_agent_id": None,
            }
        ],
        project_id="awoooi",
        limit=20,
        source_review_decisions_by_work_item={
            "source-evidence:sentry:received:issue-1": decision,
        },
    )

    item = recurrence["items"][0]
    assert recurrence["summary"]["open_work_item_group_total"] == 0
    assert recurrence["summary"]["source_correlation_review_group_total"] == 0
    assert recurrence["summary"]["source_correlation_decision_recorded_group_total"] == 1
    assert item["source_correlation_review"] == decision
    assert item["repair_summary"]["status"] == "source_correlation_accepted"
    assert item["work_item"]["status"] == "closed"
    assert item["work_item"]["matched_incident_id"] == "INC-20260520-ABC123"
    assert item["work_item"]["next_step"] == "verify_source_match_in_status_chain"


def test_source_correlation_apply_requires_accepted_review_and_plans_source_link() -> None:
    accepted_decision = {
        "schema_version": "awooop_source_correlation_review_decision_v1",
        "review_id": "review-1",
        "work_item_id": "source-evidence:sentry:received:issue-1",
        "decision": "accepted",
        "review_status": "accepted",
        "target_incident_id": "INC-20260520-ABC123",
        "reviewer_id": "operator_console",
        "latest_provider_event_id": "sentry:received:issue-1",
        "recorded_at": "2026-05-20T13:11:00",
    }
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "sentry:received:issue-1",
                "content_hash": "a" * 64,
                "content_preview": "Sentry issue",
                "content_redacted": "Sentry issue",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "sentry",
                    "stage": "received",
                    "source_refs": {
                        "sentry_issue_ids": ["issue-1"],
                        "alert_ids": ["sentry:received:issue-1"],
                    },
                    "log_correlation": {
                        "alertname": "Sentry Issue",
                        "severity": "error",
                        "namespace": "awoooi-prod",
                        "target_resource": "web",
                        "fingerprint": "fp-sentry-issue-1",
                    },
                },
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-20T13:10:00",
                "run_id": None,
                "run_state": None,
                "run_agent_id": None,
            }
        ],
        project_id="awoooi",
        limit=20,
        source_review_decisions_by_work_item={
            "source-evidence:sentry:received:issue-1": accepted_decision,
        },
    )

    apply_payload = build_source_correlation_apply(
        recurrence,
        work_item_id="source-evidence:sentry:received:issue-1",
    )

    assert apply_payload["schema_version"] == "awooop_source_correlation_apply_v1"
    assert apply_payload["allowed"] is True
    assert apply_payload["apply_status"] == "ready_to_apply"
    assert apply_payload["target_incident_id"] == "INC-20260520-ABC123"
    assert apply_payload["writes_source_event"] is True
    assert apply_payload["writes_incident_state"] is False
    assert apply_payload["writes_auto_repair_result"] is False
    assert apply_payload["writes_ticket"] is False
    assert apply_payload["source_event_stage"] == "source_correlation_linked"
    assert apply_payload["source_event_provider_event_id"] == (
        "sentry:source_correlation_linked:issue-1"
    )
    assert apply_payload["plan"]["writes"] == [
        "awooop_conversation_event",
        "timeline_events",
        "alert_operation_log",
    ]
    assert apply_payload["next_step"] == "verify_source_link_in_status_chain"


def test_source_correlation_apply_blocks_without_accepted_review() -> None:
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "sentry:received:issue-1",
                "content_hash": "a" * 64,
                "content_preview": "Sentry issue",
                "content_redacted": "Sentry issue",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "sentry",
                    "stage": "received",
                    "source_refs": {"sentry_issue_ids": ["issue-1"]},
                },
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-20T13:10:00",
                "run_id": None,
                "run_state": None,
                "run_agent_id": None,
            }
        ],
        project_id="awoooi",
        limit=20,
    )

    apply_payload = build_source_correlation_apply(
        recurrence,
        work_item_id="source-evidence:sentry:received:issue-1",
    )

    assert apply_payload["allowed"] is False
    assert apply_payload["executed"] is False
    assert apply_payload["writes_source_event"] is False
    assert apply_payload["apply_status"] == "blocked"
    assert apply_payload["plan"]["writes"] == []
    assert apply_payload["next_step"] == "fix_preflight_checks"


def test_build_dossier_recurrence_surfaces_source_apply_history() -> None:
    apply_record = {
        "schema_version": "awooop_source_correlation_apply_v1",
        "apply_id": "apply-1",
        "work_item_id": "source-evidence:sentry:received:issue-1",
        "apply_status": "applied",
        "target_incident_id": "INC-20260520-ABC123",
        "review_id": "review-1",
        "source_event_id": "source-event-1",
        "source_event_provider_event_id": (
            "sentry:source_correlation_linked:issue-1"
        ),
        "recorded_at": "2026-05-20T13:12:00",
    }
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "sentry:received:issue-1",
                "content_hash": "a" * 64,
                "content_preview": "Sentry issue",
                "content_redacted": "Sentry issue",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "sentry",
                    "stage": "received",
                    "source_refs": {"sentry_issue_ids": ["issue-1"]},
                },
                "is_duplicate": False,
                "provider_ts": None,
                "received_at": "2026-05-20T13:10:00",
                "run_id": None,
                "run_state": None,
                "run_agent_id": None,
            }
        ],
        project_id="awoooi",
        limit=20,
        source_applies_by_work_item={
            "source-evidence:sentry:received:issue-1": apply_record,
        },
    )

    item = recurrence["items"][0]
    assert recurrence["summary"]["source_correlation_applied_group_total"] == 1
    assert item["source_correlation_apply"] == apply_record
    assert item["work_item"]["matched_incident_id"] == "INC-20260520-ABC123"
    assert item["work_item"]["next_step"] == "verify_source_link_in_status_chain"


def test_build_dossier_recurrence_opens_work_item_for_completed_run_without_repair() -> None:
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "alertmanager:received:1",
                "content_hash": "a" * 64,
                "content_preview": "Docker container unhealthy",
                "content_redacted": "Docker container unhealthy",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "alertmanager",
                    "source_refs": {
                        "alert_ids": ["alert-1"],
                        "incident_ids": ["INC-20260517-F25B4A"],
                        "fingerprints": ["fp-container-unhealthy"],
                    },
                    "log_correlation": {
                        "alertname": "DockerContainerUnhealthy",
                        "severity": "warning",
                        "namespace": "momo",
                        "target_resource": "bitan-pharmacy-bitan-1",
                        "fingerprint": "fp-container-unhealthy",
                    },
                },
                "is_duplicate": True,
                "provider_ts": None,
                "received_at": "2026-05-17T23:47:00",
                "run_id": UUID("33333333-3333-4333-8333-333333333333"),
                "run_state": "completed",
                "run_agent_id": "openclaw",
            }
        ],
        project_id="awoooi",
        limit=20,
    )

    item = recurrence["items"][0]
    assert recurrence["summary"]["open_work_item_group_total"] == 1
    assert recurrence["summary"]["automation_gap_group_total"] == 1
    assert item["repair_summary"]["status"] == "run_completed_no_repair"
    assert item["work_item"] == {
        "schema_version": "awooop_recurrence_work_item_link_v1",
        "work_item_id": "incident:INC-20260517-F25B4A",
        "incident_id": "INC-20260517-F25B4A",
        "matched_incident_id": None,
        "auto_repair_id": None,
        "status": "open",
        "kind": "automation_gap",
        "next_step": "create_repair_ticket",
        "reason": "completed_run_without_auto_repair",
        "needs_human": True,
    }


def test_build_recurrence_work_item_preview_selects_ticket_mode() -> None:
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "alertmanager:received:1",
                "content_hash": "a" * 64,
                "content_preview": "Docker container unhealthy",
                "content_redacted": "Docker container unhealthy",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "alertmanager",
                    "source_refs": {
                        "alert_ids": ["alert-1"],
                        "incident_ids": ["INC-20260517-F25B4A"],
                        "fingerprints": ["fp-container-unhealthy"],
                    },
                    "log_correlation": {
                        "alertname": "DockerContainerUnhealthy",
                        "severity": "warning",
                        "namespace": "momo",
                        "target_resource": "bitan-pharmacy-bitan-1",
                        "fingerprint": "fp-container-unhealthy",
                    },
                },
                "is_duplicate": True,
                "provider_ts": None,
                "received_at": "2026-05-17T23:47:00",
                "run_id": UUID("33333333-3333-4333-8333-333333333333"),
                "run_state": "completed",
                "run_agent_id": "openclaw",
            }
        ],
        project_id="awoooi",
        limit=20,
    )

    preview = build_recurrence_work_item_preview(
        recurrence,
        work_item_id="incident:INC-20260517-F25B4A",
    )

    assert preview["schema_version"] == "awooop_recurrence_work_item_preview_v1"
    assert preview["source"] == "channel_event_dossier.recurrence"
    assert preview["mode"] == "ticket"
    assert preview["allowed"] is True
    assert preview["writes_incident_state"] is False
    assert preview["writes_auto_repair_result"] is False
    assert preview["writes_ticket"] is False
    assert preview["plan"]["step"] == "prepare_repair_ticket_preview"
    assert preview["plan"]["target"]["alertname"] == "DockerContainerUnhealthy"


def test_build_recurrence_work_item_dry_run_returns_ticket_preview_without_writes() -> None:
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "alertmanager:received:1",
                "content_hash": "a" * 64,
                "content_preview": "Docker container unhealthy",
                "content_redacted": "Docker container unhealthy",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "alertmanager",
                    "source_refs": {
                        "alert_ids": ["alert-1"],
                        "incident_ids": ["INC-20260517-F25B4A"],
                        "fingerprints": ["fp-container-unhealthy"],
                    },
                    "log_correlation": {
                        "alertname": "DockerContainerUnhealthy",
                        "severity": "warning",
                        "namespace": "momo",
                        "target_resource": "bitan-pharmacy-bitan-1",
                        "fingerprint": "fp-container-unhealthy",
                    },
                },
                "is_duplicate": True,
                "provider_ts": None,
                "received_at": "2026-05-17T23:47:00",
                "run_id": UUID("33333333-3333-4333-8333-333333333333"),
                "run_state": "completed",
                "run_agent_id": "openclaw",
            }
        ],
        project_id="awoooi",
        limit=20,
    )

    dry_run = build_recurrence_work_item_dry_run(
        recurrence,
        work_item_id="incident:INC-20260517-F25B4A",
    )

    assert dry_run["schema_version"] == "awooop_recurrence_work_item_dry_run_v1"
    assert dry_run["mode"] == "ticket"
    assert dry_run["allowed"] is True
    assert dry_run["executed"] is True
    assert dry_run["writes_incident_state"] is False
    assert dry_run["writes_auto_repair_result"] is False
    assert dry_run["writes_ticket"] is False
    assert dry_run["verification_result_preview"] == "ticket_preview_ready"
    assert dry_run["ticket_preview"]["would_create"] is False
    assert "DockerContainerUnhealthy" in dry_run["ticket_preview"]["title"]
    assert dry_run["current_state_summary"]["repair_status"] == "run_completed_no_repair"
    assert dry_run["read_model_route"]["required_scope"] == "read"


@pytest.mark.asyncio
async def test_source_correlation_dry_run_history_records_without_incident(
    monkeypatch,
) -> None:
    appended: list[dict[str, object]] = []

    class FakeRecord:
        id = "alert-op-1"

    class FakeRepo:
        async def append(self, event_type: str, **kwargs):  # noqa: ANN001
            appended.append({"event_type": event_type, **kwargs})
            return FakeRecord()

    from src.repositories import alert_operation_log_repository

    monkeypatch.setattr(
        alert_operation_log_repository,
        "get_alert_operation_log_repository",
        lambda: FakeRepo(),
    )

    history = (
        await channel_event_dossier_service._record_recurrence_work_item_dry_run_history(
            {
                "source": "channel_event_dossier.recurrence",
                "project_id": "awoooi",
                "work_item_id": (
                    "source-evidence:sentry:upstream_canary:"
                    "awoooi-canary-codex-t115-production"
                ),
                "incident_id": None,
                "auto_repair_id": None,
                "mode": "observe",
                "requested_mode": "auto",
                "allowed": True,
                "executed": True,
                "safety_level": "read_only",
                "writes_incident_state": False,
                "writes_auto_repair_result": False,
                "writes_ticket": False,
                "verification_result_preview": "observe_only",
                "current_state_summary": {
                    "work_item_kind": "source_correlation_review",
                    "work_item_next_step": "review_provider_source_match",
                    "repair_status": "source_correlation_review",
                    "latest_stage": "upstream_canary",
                    "latest_provider_event_id": (
                        "sentry:upstream_canary:"
                        "awoooi-canary-codex-t115-production"
                    ),
                    "latest_run_id": UUID("11111111-1111-4111-8111-111111111111"),
                },
                "ticket_preview": {"would_create": False},
                "read_model_route": {
                    "agent_id": "awooop_recurrence_coordinator",
                    "tool_name": "channel_event_dossier.recurrence",
                    "required_scope": "read",
                },
                "checks": [],
                "next_step": "review_provider_source_match",
            }
        )
    )

    assert history == {
        "recorded": True,
        "alert_operation_id": "alert-op-1",
        "timeline_event_id": None,
        "timeline_reason": "source_review_not_incident_scoped",
    }
    assert appended[0]["event_type"] == "PRE_FLIGHT_PASSED"
    assert appended[0]["incident_id"] is None
    assert appended[0]["actor"] == "awooop_recurrence_work_item_service"
    assert appended[0]["context"]["work_item_id"].startswith("source-evidence:")
    assert (
        appended[0]["context"]["current_state_summary"]["latest_run_id"]
        == "11111111-1111-4111-8111-111111111111"
    )


@pytest.mark.asyncio
async def test_source_correlation_apply_history_appends_source_event_and_audit(
    monkeypatch,
) -> None:
    source_calls: list[dict[str, object]] = []
    timeline_calls: list[dict[str, object]] = []
    alert_calls: list[dict[str, object]] = []

    async def fake_record_external_alert_event(**kwargs):  # noqa: ANN003
        source_calls.append(kwargs)
        return UUID("99999999-9999-4999-8999-999999999999")

    class FakeTimeline:
        async def add_event(self, **kwargs):  # noqa: ANN001
            timeline_calls.append(kwargs)
            return {"id": "timeline-1"}

    class FakeRecord:
        id = "alert-op-1"

    class FakeRepo:
        async def append(self, event_type: str, **kwargs):  # noqa: ANN001
            alert_calls.append({"event_type": event_type, **kwargs})
            return FakeRecord()

    from src.repositories import alert_operation_log_repository
    from src.services import approval_db, channel_hub

    monkeypatch.setattr(
        channel_hub,
        "record_external_alert_event",
        fake_record_external_alert_event,
    )
    monkeypatch.setattr(approval_db, "get_timeline_service", lambda: FakeTimeline())
    monkeypatch.setattr(
        alert_operation_log_repository,
        "get_alert_operation_log_repository",
        lambda: FakeRepo(),
    )

    history = await channel_event_dossier_service._record_source_correlation_apply_history(
        {
            "schema_version": "awooop_source_correlation_apply_v1",
            "source": "channel_event_dossier.recurrence",
            "project_id": "awoooi",
            "work_item_id": "source-evidence:sentry:received:issue-1",
            "review_id": "review-1",
            "target_incident_id": "INC-20260520-ABC123",
            "reviewer_id": "operator_console",
            "operator_note": "matches incident",
            "allowed": True,
            "apply_status": "ready_to_apply",
            "safety_level": "source_review_append_only_apply",
            "writes_incident_state": False,
            "writes_source_event": True,
            "writes_auto_repair_result": False,
            "writes_ticket": False,
            "creates_external_ticket": False,
            "latest_provider_event_id": "sentry:received:issue-1",
            "source_event_provider_event_id": (
                "sentry:source_correlation_linked:issue-1"
            ),
            "source_event_stage": "source_correlation_linked",
            "raw_event_id": "issue-1",
            "provider": "sentry",
            "alertname": "Sentry Issue",
            "severity": "error",
            "namespace": "awoooi-prod",
            "target_resource": "web",
            "fingerprint": "fp-sentry-issue-1",
            "checks": [],
            "current_state_summary": {},
            "plan": {"writes": ["awooop_conversation_event"]},
            "read_model_route": {},
            "next_step": "verify_source_link_in_status_chain",
        }
    )

    assert history == {
        "recorded": True,
        "source_event_id": "99999999-9999-4999-8999-999999999999",
        "alert_operation_id": "alert-op-1",
        "timeline_event_id": "timeline-1",
    }
    assert source_calls[0]["stage"] == "source_correlation_linked"
    assert source_calls[0]["incident_id"] == "INC-20260520-ABC123"
    assert source_calls[0]["event_id"] == "issue-1"
    assert timeline_calls[0]["incident_id"] == "INC-20260520-ABC123"
    assert alert_calls[0]["actor"] == "awooop_source_correlation_apply_service"
    assert alert_calls[0]["context"]["schema_version"] == (
        "awooop_source_correlation_apply_v1"
    )
    assert alert_calls[0]["context"]["apply_status"] == "applied"


def test_build_recurrence_work_item_handoff_records_ticket_proposal_contract() -> None:
    recurrence = build_dossier_recurrence(
        [
            {
                "event_id": "event-1",
                "project_id": "awoooi",
                "channel_type": "internal",
                "provider_event_id": "alertmanager:received:1",
                "content_hash": "a" * 64,
                "content_preview": "Docker container unhealthy",
                "content_redacted": "Docker container unhealthy",
                "redaction_version": "audit_sink_v1",
                "source_envelope": {
                    "provider": "alertmanager",
                    "source_refs": {
                        "alert_ids": ["alert-1"],
                        "incident_ids": ["INC-20260517-F25B4A"],
                        "fingerprints": ["fp-container-unhealthy"],
                    },
                    "log_correlation": {
                        "alertname": "DockerContainerUnhealthy",
                        "severity": "warning",
                        "namespace": "momo",
                        "target_resource": "bitan-pharmacy-bitan-1",
                        "fingerprint": "fp-container-unhealthy",
                    },
                },
                "is_duplicate": True,
                "provider_ts": None,
                "received_at": "2026-05-17T23:47:00",
                "run_id": UUID("33333333-3333-4333-8333-333333333333"),
                "run_state": "completed",
                "run_agent_id": "openclaw",
            }
        ],
        project_id="awoooi",
        limit=20,
    )

    handoff = build_recurrence_work_item_handoff(
        recurrence,
        work_item_id="incident:INC-20260517-F25B4A",
    )

    assert handoff["schema_version"] == "awooop_recurrence_work_item_handoff_v1"
    assert handoff["mode"] == "ticket"
    assert handoff["handoff_kind"] == "ticket_proposal"
    assert handoff["handoff_status"] == "ready_to_record"
    assert handoff["handoff_owner"] == "operator"
    assert handoff["safety_level"] == "handoff_record_only"
    assert handoff["allowed"] is True
    assert handoff["creates_external_ticket"] is False
    assert handoff["writes_incident_state"] is False
    assert handoff["writes_auto_repair_result"] is False
    assert handoff["writes_ticket"] is False
    assert handoff["ticket_preview"]["would_create"] is False
    assert handoff["next_step"] == "operator_review_ticket_preview"
    assert handoff["plan"]["step"] == "record_recurrence_work_item_handoff"
    assert handoff["plan"]["writes"] == ["timeline_events", "alert_operation_log"]
    assert handoff["read_model_route"]["required_scope"] == "record_history"


def test_build_recurrence_work_item_preview_raises_for_missing_item() -> None:
    recurrence = build_dossier_recurrence(
        [],
        project_id="awoooi",
        limit=20,
    )

    with pytest.raises(RecurrenceWorkItemNotFoundError):
        build_recurrence_work_item_preview(
            recurrence,
            work_item_id="incident:INC-20260517-MISSING",
        )


def test_recurrence_response_model_preserves_repair_work_item_fields() -> None:
    response = ChannelEventRecurrenceResponse.model_validate(
        {
            "project_id": "awoooi",
            "limit": 20,
            "summary": {
                "source_event_total": 1,
                "recurrence_group_total": 1,
                "recurrent_group_total": 1,
                "duplicate_event_total": 0,
                "linked_run_total": 1,
                "unlinked_event_total": 0,
                "auto_repair_linked_total": 1,
                "verified_repair_group_total": 1,
                "open_work_item_group_total": 0,
                "manual_gate_group_total": 0,
                "automation_gap_group_total": 0,
                "failed_repair_group_total": 0,
                "source_correlation_review_group_total": 0,
                "latest_received_at": "2026-05-13T13:47:00",
            },
            "items": [
                {
                    "recurrence_key": "fingerprint:fp-host-disk",
                    "provider": "alertmanager",
                    "alertname": "HostDiskUsageHigh",
                    "severity": "warning",
                    "namespace": "node",
                    "target_resource": "host-110",
                    "fingerprint": "fp-host-disk",
                    "latest_stage": "incident_linked",
                    "latest_event_id": "11111111-1111-4111-8111-111111111111",
                    "latest_provider_event_id": "alertmanager:received:1",
                    "latest_content_preview": "Host disk pressure",
                    "latest_run_id": "22222222-2222-4222-8222-222222222222",
                    "latest_run_state": "completed",
                    "latest_agent_id": "openclaw",
                    "latest_incident_id": "INC-20260513-ABCD",
                    "incident_ids": ["INC-20260513-ABCD"],
                    "repair_summary": {
                        "status": "auto_repair_verified",
                        "latest_auto_repair_id": "repair-1",
                    },
                    "work_item": {
                        "work_item_id": "verification:INC-20260513-ABCD:repair-1",
                        "status": "closed",
                    },
                    "occurrence_total": 1,
                    "duplicate_total": 0,
                    "linked_run_total": 1,
                    "source_ref_total": 3,
                    "missing_source_refs_total": 0,
                    "sentry_ref_total": 0,
                    "signoz_ref_total": 0,
                    "alert_ref_total": 1,
                    "stage_counts": {"incident_linked": 1},
                    "run_state_counts": {"completed": 1},
                    "first_received_at": "2026-05-13T13:47:00",
                    "latest_received_at": "2026-05-13T13:47:00",
                }
            ],
        }
    )

    payload = response.model_dump()
    assert payload["summary"]["auto_repair_linked_total"] == 1
    assert payload["summary"]["source_correlation_review_group_total"] == 0
    assert payload["items"][0]["latest_incident_id"] == "INC-20260513-ABCD"
    assert payload["items"][0]["latest_stage"] == "incident_linked"
    assert payload["items"][0]["stage_counts"] == {"incident_linked": 1}
    assert payload["items"][0]["repair_summary"]["status"] == "auto_repair_verified"
    assert payload["items"][0]["work_item"]["status"] == "closed"


@pytest.mark.asyncio
async def test_fetch_channel_event_dossier_requires_source() -> None:
    with pytest.raises(HTTPException) as exc_info:
        await fetch_channel_event_dossier(
            project_id="awoooi",
            run_id=None,
            provider_event_id=None,
            limit=20,
        )

    assert exc_info.value.status_code == 422


@pytest.mark.asyncio
async def test_fetch_channel_event_dossier_uses_typed_run_filter(monkeypatch) -> None:
    captured: dict[str, object] = {}

    class FakeMappings:
        def all(self) -> list[dict[str, object]]:
            return []

    class FakeResult:
        def mappings(self) -> FakeMappings:
            return FakeMappings()

    class FakeDb:
        async def execute(self, statement, params):  # noqa: ANN001
            captured["sql"] = str(statement)
            captured["params"] = params
            return FakeResult()

    class FakeContext:
        async def __aenter__(self) -> FakeDb:
            return FakeDb()

        async def __aexit__(self, exc_type, exc, tb) -> None:  # noqa: ANN001
            return None

    monkeypatch.setattr(
        channel_event_dossier_service,
        "get_db_context",
        lambda _project_id: FakeContext(),
    )

    run_id = UUID("0a4c365f-609e-5441-bc29-4c7ebc3603b6")
    result = await fetch_channel_event_dossier(
        project_id="awoooi",
        run_id=run_id,
        provider_event_id=None,
        limit=20,
    )

    assert result["total"] == 0
    assert "run_id = CAST(:run_id AS uuid)" in str(captured["sql"])
    assert ":run_id IS NULL" not in str(captured["sql"])
    assert captured["params"] == {
        "project_id": "awoooi",
        "run_id": str(run_id),
        "limit": 20,
    }


@pytest.mark.asyncio
async def test_fetch_channel_event_dossier_coverage_uses_typed_provider_filter(
    monkeypatch,
) -> None:
    captured: list[dict[str, object]] = []

    class FakeMappings:
        def all(self) -> list[dict[str, object]]:
            return []

    class FakeResult:
        def mappings(self) -> FakeMappings:
            return FakeMappings()

    class FakeDb:
        async def execute(self, statement, params):  # noqa: ANN001
            captured.append({"sql": str(statement), "params": params})
            return FakeResult()

    class FakeContext:
        async def __aenter__(self) -> FakeDb:
            return FakeDb()

        async def __aexit__(self, exc_type, exc, tb) -> None:  # noqa: ANN001
            return None

    monkeypatch.setattr(
        channel_event_dossier_service,
        "get_db_context",
        lambda _project_id: FakeContext(),
    )

    result = await fetch_channel_event_dossier_coverage(
        project_id=None,
        provider="sentry",
        limit=500,
    )

    assert result["project_id"] == "awoooi"
    assert result["limit"] == 200
    coverage_query = captured[0]
    assert "source_envelope->>'provider'" in str(coverage_query["sql"])
    assert coverage_query["params"] == {
        "project_id": "awoooi",
        "provider": "sentry",
        "limit": 200,
    }


@pytest.mark.asyncio
async def test_fetch_channel_event_dossier_recurrence_uses_joined_typed_filter(
    monkeypatch,
) -> None:
    captured: list[dict[str, object]] = []

    class FakeMappings:
        def all(self) -> list[dict[str, object]]:
            return []

    class FakeResult:
        def mappings(self) -> FakeMappings:
            return FakeMappings()

    class FakeDb:
        async def execute(self, statement, params):  # noqa: ANN001
            captured.append({"sql": str(statement), "params": params})
            return FakeResult()

    class FakeContext:
        async def __aenter__(self) -> FakeDb:
            return FakeDb()

        async def __aexit__(self, exc_type, exc, tb) -> None:  # noqa: ANN001
            return None

    monkeypatch.setattr(
        channel_event_dossier_service,
        "get_db_context",
        lambda _project_id: FakeContext(),
    )

    result = await fetch_channel_event_dossier_recurrence(
        project_id="awoooi",
        provider="alertmanager",
        limit=500,
    )

    assert result["project_id"] == "awoooi"
    assert result["limit"] == 300
    recurrence_query = captured[0]
    assert "LEFT JOIN awooop_run_state r" in str(recurrence_query["sql"])
    assert "e.source_envelope->>'provider'" in str(recurrence_query["sql"])
    assert ":provider IS NULL" not in str(recurrence_query["sql"])
    assert recurrence_query["params"] == {
        "project_id": "awoooi",
        "provider": "alertmanager",
        "limit": 300,
    }
