"""Tests for payload-first external export generation."""

from __future__ import annotations

import json
from pathlib import Path

import httpx
import pytest

from eu_ai_act.checker import ComplianceChecker, ComplianceStatus
from eu_ai_act.exporter import ExportGenerator, ExportPusher, ExportPushError
from eu_ai_act.history import build_event
from eu_ai_act.schema import load_system_descriptor_from_file

REPO_ROOT = Path(__file__).resolve().parents[1]
EXAMPLES_DIR = REPO_ROOT / "examples"


def test_from_check_generic_contract_and_status_actionability():
    """Check-source export should expose canonical contract with normalized statuses."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)

    envelope = ExportGenerator().from_check(report=report, target="generic")
    payload = envelope.to_dict()

    assert payload["schema_version"] == "1.0"
    assert payload["source_type"] == "check"
    assert payload["target"] == "generic"
    assert payload["system_name"] == descriptor.name
    assert payload["risk_tier"] == "high_risk"

    summary = payload["summary"]
    assert summary["total_requirements"] == 6
    assert "compliance_percentage" in summary

    items = payload["items"]
    assert len(items) == 6
    for item in items:
        assert item["status"] in {"non_compliant", "partial", "not_assessed", "compliant"}
        assert item["actionable"] == (
            item["status"] in {"non_compliant", "partial", "not_assessed"}
        )
        assert item["article"].startswith("Art.")

    adapter_payload = payload["adapter_payload"]
    assert adapter_payload["format"] == "generic/v1"
    assert len(adapter_payload["records"]) == len(items)


def test_from_check_can_include_descriptor_path_metadata():
    """Check-source export should preserve descriptor identity when provided."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)

    payload = (
        ExportGenerator()
        .from_check(
            report=report,
            target="generic",
            descriptor_path="/tmp/medical_diagnosis.yaml",
        )
        .to_dict()
    )

    assert payload["descriptor_path"] == "/tmp/medical_diagnosis.yaml"


def test_from_check_jira_and_servicenow_adapters_only_include_actionable_items():
    """Jira and ServiceNow payloads should generate records only for actionable items."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    generator = ExportGenerator()

    jira_payload = generator.from_check(report=report, target="jira").to_dict()
    servicenow_payload = generator.from_check(report=report, target="servicenow").to_dict()

    actionable_count = sum(1 for item in jira_payload["items"] if item["actionable"])

    assert jira_payload["adapter_payload"]["format"] == "jira/issues/v1"
    assert len(jira_payload["adapter_payload"]["issues"]) == actionable_count
    if actionable_count:
        jira_issue_fields = jira_payload["adapter_payload"]["issues"][0]["fields"]
        assert "summary" in jira_issue_fields
        assert "description" in jira_issue_fields
        assert "labels" in jira_issue_fields

    assert servicenow_payload["adapter_payload"]["format"] == "servicenow/records/v1"
    assert len(servicenow_payload["adapter_payload"]["records"]) == actionable_count
    if actionable_count:
        snow_record = servicenow_payload["adapter_payload"]["records"][0]
        assert "short_description" in snow_record
        assert "description" in snow_record
        assert snow_record["category"] == "compliance"


def test_from_history_preserves_event_metadata():
    """History-source export should include history metadata and normalized items."""
    event = build_event(
        event_type="check",
        system_name="Snapshot System",
        descriptor_path="/tmp/snapshot.yaml",
        risk_tier="limited",
        summary={
            "total_requirements": 2,
            "compliant_count": 1,
            "non_compliant_count": 0,
            "partial_count": 1,
            "not_assessed_count": 0,
            "compliance_percentage": 75.0,
        },
        finding_statuses={"Art. 50": "partial", "Art. 11": "compliant"},
    )

    payload = ExportGenerator().from_history(event=event, target="servicenow").to_dict()

    assert payload["source_type"] == "history"
    assert payload["event_id"] == event.event_id
    assert payload["event_type"] == "check"
    assert payload["descriptor_path"] == "/tmp/snapshot.yaml"
    assert payload["history_generated_at"] == event.generated_at
    assert payload["summary"]["partial_count"] == 1
    assert len(payload["items"]) == 2
    assert payload["adapter_payload"]["format"] == "servicenow/records/v1"
    assert payload["adapter_payload"]["actionable_count"] == 1


def test_invalid_target_raises_value_error():
    """Unsupported adapter target should fail deterministically."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "spam_filter.yaml")
    report = ComplianceChecker().check(descriptor)

    with pytest.raises(ValueError, match="Unsupported export target"):
        ExportGenerator().from_check(report=report, target="unknown")  # type: ignore[arg-type]


def test_invalid_history_status_raises_value_error():
    """Unsupported status values in history events should fail export generation."""
    event = build_event(
        event_type="check",
        system_name="Broken Snapshot",
        descriptor_path="/tmp/broken.yaml",
        risk_tier="minimal",
        summary={
            "total_requirements": 1,
            "compliant_count": 0,
            "non_compliant_count": 0,
            "partial_count": 0,
            "not_assessed_count": 1,
            "compliance_percentage": 0.0,
        },
        finding_statuses={"Art. 69": "blocked"},
    )

    with pytest.raises(ValueError, match="Unsupported status value"):
        ExportGenerator().from_history(event=event, target="generic")


def test_push_rejects_generic_target():
    """Live push should fail for generic target by contract."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "spam_filter.yaml")
    report = ComplianceChecker().check(descriptor)
    envelope = ExportGenerator().from_check(report=report, target="generic")

    with pytest.raises(ValueError, match="Live push is not supported for target 'generic'"):
        ExportPusher(idempotency_enabled=False).push(envelope)


def test_push_dry_run_for_jira_requires_no_credentials():
    """Dry-run should not require environment credentials and should return deterministic summary."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    envelope = ExportGenerator().from_check(report=report, target="jira")

    result = ExportPusher(idempotency_enabled=False).push(envelope, dry_run=True)

    assert result["target"] == "jira"
    assert result["dry_run"] is True
    assert result["attempted_actionable_count"] == sum(
        1 for item in envelope.items if item.actionable
    )
    assert result["pushed_count"] == 0
    assert result["failed_count"] == 0


def test_push_jira_missing_env_raises_value_error(monkeypatch):
    """Live Jira push should fail-fast when required environment variables are missing."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    envelope = ExportGenerator().from_check(report=report, target="jira")

    for key in [
        "EU_AI_ACT_JIRA_BASE_URL",
        "EU_AI_ACT_JIRA_EMAIL",
        "EU_AI_ACT_JIRA_API_TOKEN",
        "EU_AI_ACT_JIRA_PROJECT_KEY",
    ]:
        monkeypatch.delenv(key, raising=False)

    with pytest.raises(
        ValueError, match="Missing required environment variable: EU_AI_ACT_JIRA_BASE_URL"
    ):
        ExportPusher(idempotency_enabled=False).push(envelope, dry_run=False)


def test_push_jira_success_with_mock_transport(monkeypatch):
    """Live Jira push should report success counts when remote API responds 201."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    envelope = ExportGenerator().from_check(report=report, target="jira")
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    def handler(request: httpx.Request) -> httpx.Response:
        assert request.method == "POST"
        assert str(request.url) == "https://example.atlassian.net/rest/api/3/issue"
        return httpx.Response(status_code=201, json={"key": "EUAI-100"})

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    result = ExportPusher(idempotency_enabled=False).push(envelope)
    assert result["target"] == "jira"
    assert result["dry_run"] is False
    assert result["attempted_actionable_count"] == len(envelope.adapter_payload["issues"])
    assert result["pushed_count"] == len(envelope.adapter_payload["issues"])
    assert result["failed_count"] == 0
    assert result["failure_reason"] is None


def test_push_jira_retries_on_5xx_then_succeeds(monkeypatch):
    """Retryable 5xx responses should be retried with eventual success."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        call_counter["count"] += 1
        if call_counter["count"] == 1:
            return httpx.Response(status_code=503, text="temporary outage")
        return httpx.Response(status_code=201, json={"key": f"EUAI-{call_counter['count']}"})

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)
    monkeypatch.setattr(ExportPusher, "_sleep_before_retry", lambda _self, _attempts: None)

    result = ExportPusher(max_retries=3, idempotency_enabled=False).push(envelope)
    issue_count = len(envelope.adapter_payload["issues"])
    assert result["pushed_count"] == issue_count
    assert result["failed_count"] == 0
    assert result["results"][0]["attempts"] == 2
    assert result["results"][0]["retries_used"] == 1
    assert call_counter["count"] == issue_count + 1


def test_push_jira_retries_on_429_then_succeeds(monkeypatch):
    """Retryable 429 responses should be retried before succeeding."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        call_counter["count"] += 1
        if call_counter["count"] == 1:
            return httpx.Response(status_code=429, text="rate limit")
        return httpx.Response(status_code=201, json={"key": f"EUAI-{call_counter['count']}"})

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)
    monkeypatch.setattr(ExportPusher, "_sleep_before_retry", lambda _self, _attempts: None)

    result = ExportPusher(max_retries=3, idempotency_enabled=False).push(envelope)
    issue_count = len(envelope.adapter_payload["issues"])
    assert result["pushed_count"] == issue_count
    assert result["failed_count"] == 0
    assert result["results"][0]["attempts"] == 2
    assert result["results"][0]["retries_used"] == 1
    assert call_counter["count"] == issue_count + 1


def test_push_jira_retries_on_transport_error_then_succeeds(monkeypatch):
    """Transport errors should be retried with backoff before eventual success."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        call_counter["count"] += 1
        if call_counter["count"] == 1:
            raise httpx.TransportError("connection reset by peer")
        return httpx.Response(status_code=201, json={"key": f"EUAI-{call_counter['count']}"})

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)
    monkeypatch.setattr(ExportPusher, "_sleep_before_retry", lambda _self, _attempts: None)

    result = ExportPusher(max_retries=3, idempotency_enabled=False).push(envelope)
    issue_count = len(envelope.adapter_payload["issues"])
    assert result["pushed_count"] == issue_count
    assert result["failed_count"] == 0
    assert result["results"][0]["attempts"] == 2
    assert result["results"][0]["retries_used"] == 1
    assert call_counter["count"] == issue_count + 1


def test_push_jira_retry_exhaustion_fails_fast(monkeypatch):
    """Retryable failures should abort after retries are exhausted on first failing item."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"count": 0}

    def handler(_request: httpx.Request) -> httpx.Response:
        call_counter["count"] += 1
        return httpx.Response(status_code=503, text="temporary outage")

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)
    monkeypatch.setattr(ExportPusher, "_sleep_before_retry", lambda _self, _attempts: None)

    with pytest.raises(ExportPushError) as exc_info:
        ExportPusher(max_retries=2, idempotency_enabled=False).push(envelope)

    push_result = exc_info.value.push_result
    assert push_result["failed_count"] == 1
    assert push_result["pushed_count"] == 0
    assert "HTTP 503" in push_result["failure_reason"]
    assert push_result["results"][0]["attempts"] == 3
    assert call_counter["count"] == 3


def test_push_jira_does_not_retry_non_retryable_4xx(monkeypatch):
    """Non-retryable 4xx responses should fail immediately without retry loop."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"count": 0}

    def handler(_request: httpx.Request) -> httpx.Response:
        call_counter["count"] += 1
        return httpx.Response(status_code=400, text="bad request")

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)
    monkeypatch.setattr(ExportPusher, "_sleep_before_retry", lambda _self, _attempts: None)

    with pytest.raises(ExportPushError) as exc_info:
        ExportPusher(max_retries=5, idempotency_enabled=False).push(envelope)

    push_result = exc_info.value.push_result
    assert push_result["failed_count"] == 1
    assert push_result["results"][0]["attempts"] == 1
    assert "HTTP 400" in push_result["failure_reason"]
    assert call_counter["count"] == 1


def test_push_jira_upsert_lookup_miss_creates(monkeypatch):
    """Upsert mode should create new Jira issues when lookup finds no existing issue."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"search": 0, "create": 0, "update": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        if request.method == "GET":
            call_counter["search"] += 1
            return httpx.Response(status_code=200, json={"issues": []})
        if request.method == "POST":
            call_counter["create"] += 1
            payload = json.loads(request.content.decode("utf-8"))
            labels = payload["fields"].get("labels", [])
            assert any(label.startswith("eu-ai-act-idem-") for label in labels)
            return httpx.Response(status_code=201, json={"key": f"EUAI-{call_counter['create']}"})
        call_counter["update"] += 1
        return httpx.Response(status_code=500, text="unexpected method")

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    result = ExportPusher(idempotency_enabled=False, max_retries=0).push(
        envelope,
        push_mode="upsert",
    )
    issue_count = len(envelope.adapter_payload["issues"])
    assert result["push_mode"] == "upsert"
    assert result["pushed_count"] == issue_count
    assert result["created_count"] == issue_count
    assert result["updated_count"] == 0
    assert call_counter["search"] == issue_count
    assert call_counter["create"] == issue_count
    assert call_counter["update"] == 0
    assert all(item["operation"] == "created" for item in result["results"])


def test_push_jira_upsert_lookup_hit_updates(monkeypatch):
    """Upsert mode should update Jira issues when lookup matches by idempotency label."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"search": 0, "create": 0, "update": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        if request.method == "GET":
            call_counter["search"] += 1
            return httpx.Response(
                status_code=200,
                json={"issues": [{"key": f"EUAI-{call_counter['search']}"}]},
            )
        if request.method == "PUT":
            call_counter["update"] += 1
            return httpx.Response(status_code=204, text="")
        call_counter["create"] += 1
        return httpx.Response(status_code=500, text="unexpected method")

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    result = ExportPusher(idempotency_enabled=False, max_retries=0).push(
        envelope,
        push_mode="upsert",
    )
    issue_count = len(envelope.adapter_payload["issues"])
    assert result["push_mode"] == "upsert"
    assert result["pushed_count"] == issue_count
    assert result["created_count"] == 0
    assert result["updated_count"] == issue_count
    assert call_counter["search"] == issue_count
    assert call_counter["update"] == issue_count
    assert call_counter["create"] == 0
    assert all(item["operation"] == "updated" for item in result["results"])


def test_push_jira_upsert_lookup_non_retryable_4xx_fails_fast(monkeypatch):
    """Non-retryable 4xx during Jira lookup should abort immediately in upsert mode."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for finding in report.findings.values():
        finding.status = ComplianceStatus.COMPLIANT
    first_requirement = next(iter(report.findings.keys()))
    report.findings[first_requirement].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"search": 0, "create": 0, "update": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        if request.method == "GET":
            call_counter["search"] += 1
            return httpx.Response(status_code=400, text="bad search request")
        if request.method == "POST":
            call_counter["create"] += 1
        if request.method == "PUT":
            call_counter["update"] += 1
        return httpx.Response(status_code=500, text="unexpected method")

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    with pytest.raises(ExportPushError) as exc_info:
        ExportPusher(idempotency_enabled=False, max_retries=3).push(
            envelope,
            push_mode="upsert",
        )

    push_result = exc_info.value.push_result
    assert push_result["failed_count"] == 1
    assert push_result["created_count"] == 0
    assert push_result["updated_count"] == 0
    assert push_result["results"][0]["operation"] == "lookup"
    assert push_result["results"][0]["attempts"] == 1
    assert "HTTP 400" in push_result["failure_reason"]
    assert call_counter["search"] == 1
    assert call_counter["create"] == 0
    assert call_counter["update"] == 0


def test_push_jira_duplicate_items_are_skipped_via_idempotency_ledger(monkeypatch, tmp_path):
    """Second push with same payload should skip duplicates and avoid remote calls."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        call_counter["count"] += 1
        return httpx.Response(status_code=201, json={"key": f"EUAI-{call_counter['count']}"})

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    ledger_path = tmp_path / "export_push_ledger.jsonl"
    pusher = ExportPusher(idempotency_path=ledger_path, max_retries=0)
    first = pusher.push(envelope)
    second = pusher.push(envelope)

    actionable_count = len(envelope.adapter_payload["issues"])
    assert first["pushed_count"] == actionable_count
    assert first["skipped_duplicate_count"] == 0
    assert second["pushed_count"] == 0
    assert second["skipped_duplicate_count"] == actionable_count
    assert all(item["status"] == "skipped_duplicate" for item in second["results"])
    assert call_counter["count"] == actionable_count
    assert ledger_path.exists()
    assert len(ledger_path.read_text(encoding="utf-8").strip().splitlines()) == actionable_count


def test_push_jira_check_source_idempotency_uses_descriptor_identity(monkeypatch, tmp_path):
    """Different descriptor paths must produce different idempotency keys for check exports."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT

    envelope_a = ExportGenerator().from_check(
        report=report,
        target="jira",
        descriptor_path="/tmp/system_a.yaml",
    )
    envelope_b = ExportGenerator().from_check(
        report=report,
        target="jira",
        descriptor_path="/tmp/system_b.yaml",
    )

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"count": 0}

    def handler(_request: httpx.Request) -> httpx.Response:
        call_counter["count"] += 1
        return httpx.Response(status_code=201, json={"key": f"EUAI-{call_counter['count']}"})

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    ledger_path = tmp_path / "export_push_ledger.jsonl"
    pusher = ExportPusher(idempotency_path=ledger_path, max_retries=0)

    first = pusher.push(envelope_a)
    second = pusher.push(envelope_b)

    actionable_count = len(envelope_a.adapter_payload["issues"])
    assert first["pushed_count"] == actionable_count
    assert second["pushed_count"] == actionable_count
    assert second["skipped_duplicate_count"] == 0
    assert call_counter["count"] == actionable_count * 2


def test_push_jira_success_with_ledger_write_error_keeps_success(monkeypatch, tmp_path):
    """Ledger write failures after remote success should not flip the command to failed."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    def handler(_request: httpx.Request) -> httpx.Response:
        return httpx.Response(status_code=201, json={"key": "EUAI-100"})

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    pusher = ExportPusher(idempotency_path=tmp_path / "ledger.jsonl", max_retries=0)
    monkeypatch.setattr(
        pusher,
        "_append_ledger_record",
        lambda _record: (_ for _ in ()).throw(OSError("read-only filesystem")),
    )

    result = pusher.push(envelope)

    actionable_count = len(envelope.adapter_payload["issues"])
    assert result["pushed_count"] == actionable_count
    assert result["failed_count"] == 0
    assert all(item["status"] == "success" for item in result["results"])
    assert all(item["ledger_recorded"] is False for item in result["results"])
    assert all("read-only filesystem" in item["ledger_error"] for item in result["results"])


def test_push_dry_run_does_not_write_idempotency_ledger(tmp_path):
    """Dry-run must not create or append to idempotency ledger files."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    envelope = ExportGenerator().from_check(report=report, target="jira")

    ledger_path = tmp_path / "export_push_ledger.jsonl"
    result = ExportPusher(idempotency_path=ledger_path).push(envelope, dry_run=True)

    assert result["dry_run"] is True
    assert result["idempotency_enabled"] is True
    assert result["idempotency_path"] == str(ledger_path)
    assert result["skipped_duplicate_count"] == 0
    assert not ledger_path.exists()


def test_push_disable_idempotency_allows_repeat_pushes(monkeypatch, tmp_path):
    """When idempotency is disabled, repeated pushes should call remote every time."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="jira")

    monkeypatch.setenv("EU_AI_ACT_JIRA_BASE_URL", "https://example.atlassian.net")
    monkeypatch.setenv("EU_AI_ACT_JIRA_EMAIL", "ops@example.com")
    monkeypatch.setenv("EU_AI_ACT_JIRA_API_TOKEN", "secret")
    monkeypatch.setenv("EU_AI_ACT_JIRA_PROJECT_KEY", "EUAI")

    call_counter = {"count": 0}

    def handler(_request: httpx.Request) -> httpx.Response:
        call_counter["count"] += 1
        return httpx.Response(status_code=201, json={"key": f"EUAI-{call_counter['count']}"})

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    ledger_path = tmp_path / "export_push_ledger.jsonl"
    pusher = ExportPusher(idempotency_enabled=False, idempotency_path=ledger_path, max_retries=0)
    first = pusher.push(envelope)
    second = pusher.push(envelope)

    actionable_count = len(envelope.adapter_payload["issues"])
    assert first["idempotency_enabled"] is False
    assert first["idempotency_path"] is None
    assert first["pushed_count"] == actionable_count
    assert second["pushed_count"] == actionable_count
    assert call_counter["count"] == actionable_count * 2
    assert not ledger_path.exists()


def test_push_servicenow_duplicate_items_are_skipped_via_idempotency_ledger(monkeypatch, tmp_path):
    """ServiceNow push should apply the same duplicate-skip idempotency behavior."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="servicenow")

    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_INSTANCE_URL", "https://snow.example.com")
    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_USERNAME", "ops")
    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_PASSWORD", "secret")

    call_counter = {"count": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        call_counter["count"] += 1
        assert str(request.url) == "https://snow.example.com/api/now/table/u_ai_act_compliance"
        return httpx.Response(
            status_code=201,
            json={"result": {"sys_id": f"SYS-{call_counter['count']}"}},
        )

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    ledger_path = tmp_path / "export_push_ledger.jsonl"
    pusher = ExportPusher(idempotency_path=ledger_path, max_retries=0)
    first = pusher.push(envelope)
    second = pusher.push(envelope)

    actionable_count = len(envelope.adapter_payload["records"])
    assert first["target"] == "servicenow"
    assert first["pushed_count"] == actionable_count
    assert first["skipped_duplicate_count"] == 0
    assert second["pushed_count"] == 0
    assert second["skipped_duplicate_count"] == actionable_count
    assert all(item["status"] == "skipped_duplicate" for item in second["results"])
    assert call_counter["count"] == actionable_count


def test_push_servicenow_upsert_lookup_miss_creates(monkeypatch):
    """Upsert mode should create ServiceNow records when lookup does not match."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="servicenow")

    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_INSTANCE_URL", "https://snow.example.com")
    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_USERNAME", "ops")
    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_PASSWORD", "secret")

    call_counter = {"lookup": 0, "create": 0, "update": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        if request.method == "GET":
            call_counter["lookup"] += 1
            return httpx.Response(status_code=200, json={"result": []})
        if request.method == "POST":
            call_counter["create"] += 1
            payload = json.loads(request.content.decode("utf-8"))
            assert "u_idempotency_key" in payload
            return httpx.Response(
                status_code=201,
                json={"result": {"sys_id": f"SYS-{call_counter['create']}"}},
            )
        call_counter["update"] += 1
        return httpx.Response(status_code=500, text="unexpected method")

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    result = ExportPusher(idempotency_enabled=False, max_retries=0).push(
        envelope,
        push_mode="upsert",
    )
    record_count = len(envelope.adapter_payload["records"])
    assert result["push_mode"] == "upsert"
    assert result["pushed_count"] == record_count
    assert result["created_count"] == record_count
    assert result["updated_count"] == 0
    assert call_counter["lookup"] == record_count
    assert call_counter["create"] == record_count
    assert call_counter["update"] == 0
    assert all(item["operation"] == "created" for item in result["results"])


def test_push_servicenow_upsert_lookup_hit_updates_with_custom_lookup_field(monkeypatch):
    """Upsert mode should update ServiceNow records using configured idempotency lookup field."""
    descriptor = load_system_descriptor_from_file(EXAMPLES_DIR / "medical_diagnosis.yaml")
    report = ComplianceChecker().check(descriptor)
    for requirement_id in list(report.findings.keys()):
        report.findings[requirement_id].status = ComplianceStatus.NON_COMPLIANT
    envelope = ExportGenerator().from_check(report=report, target="servicenow")

    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_INSTANCE_URL", "https://snow.example.com")
    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_USERNAME", "ops")
    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_PASSWORD", "secret")
    monkeypatch.setenv("EU_AI_ACT_SERVICENOW_IDEMPOTENCY_FIELD", "u_custom_idem")

    call_counter = {"lookup": 0, "create": 0, "update": 0}

    def handler(request: httpx.Request) -> httpx.Response:
        if request.method == "GET":
            call_counter["lookup"] += 1
            assert "u_custom_idem" in str(request.url)
            return httpx.Response(
                status_code=200,
                json={"result": [{"sys_id": f"SYS-{call_counter['lookup']}"}]},
            )
        if request.method == "PATCH":
            call_counter["update"] += 1
            payload = json.loads(request.content.decode("utf-8"))
            assert "u_custom_idem" in payload
            return httpx.Response(
                status_code=200,
                json={"result": {"sys_id": f"SYS-{call_counter['update']}"}},
            )
        call_counter["create"] += 1
        return httpx.Response(status_code=500, text="unexpected method")

    transport = httpx.MockTransport(handler)
    original_client = httpx.Client

    def _fake_client(*args, **kwargs):
        return original_client(transport=transport)

    monkeypatch.setattr("eu_ai_act.exporter.httpx.Client", _fake_client)

    result = ExportPusher(idempotency_enabled=False, max_retries=0).push(
        envelope,
        push_mode="upsert",
    )
    record_count = len(envelope.adapter_payload["records"])
    assert result["push_mode"] == "upsert"
    assert result["pushed_count"] == record_count
    assert result["created_count"] == 0
    assert result["updated_count"] == record_count
    assert call_counter["lookup"] == record_count
    assert call_counter["update"] == record_count
    assert call_counter["create"] == 0
    assert all(item["operation"] == "updated" for item in result["results"])
