//! End-to-end snapshot tests for the labeler conformance suite covering AC5 and AC6.

mod common;

use async_trait::async_trait;
use atproto_devtool::commands::test::labeler::crypto::canonicalize_label_for_signing;
use atproto_devtool::commands::test::labeler::pipeline::{
    HttpTee, LabelerOptions, parse_target, run_pipeline,
};
use atproto_devtool::common::identity::{DnsResolver, HttpClient, IdentityError};
use atrium_api::com::atproto::label::defs::{Label, LabelData};
use atrium_api::com::atproto::label::query_labels::{Output, OutputData};
use atrium_api::types::string::Datetime;
use k256::ecdsa::SigningKey as K256SigningKey;
use k256::ecdsa::signature::hazmat::PrehashSigner;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use url::Url;

/// Deterministic k256 key derived from a fixed 32-byte seed.
///
/// Returns `(signing_key, multikey_string)` where `multikey_string` is the
/// base58btc-encoded `varint(0xe7, 0x01) || compressed-SEC1` form that the
/// did:plc audit log and `publicKeyMultibase` use.
fn deterministic_k256_multikey(seed: [u8; 32]) -> (K256SigningKey, String) {
    let signing_key = K256SigningKey::from_slice(&seed).expect("valid k256 secret scalar");
    let compressed = signing_key
        .verifying_key()
        .to_encoded_point(true)
        .as_bytes()
        .to_vec();
    assert_eq!(
        compressed.len(),
        33,
        "k256 compressed SEC1 must be 33 bytes"
    );
    let mut multikey_bytes = Vec::with_capacity(2 + compressed.len());
    multikey_bytes.extend_from_slice(&[0xe7, 0x01]);
    multikey_bytes.extend_from_slice(&compressed);
    let multikey = multibase::encode(multibase::Base::Base58Btc, &multikey_bytes);
    (signing_key, multikey)
}

/// Sign a label with the given key and return a new `Label` containing the
/// signature bytes. The input `label_data` should have `sig` set to a 64-byte
/// placeholder — it will be replaced by the real signature before the label
/// is returned.
fn sign_label_with_key(mut label_data: LabelData, signing_key: &K256SigningKey) -> Label {
    label_data.sig = Some(vec![0u8; 64]);
    let placeholder: Label = label_data.clone().into();
    let canonical =
        canonicalize_label_for_signing(&placeholder).expect("canonicalize placeholder label");
    let sig: k256::ecdsa::Signature = signing_key
        .sign_prehash(&canonical.prehash)
        .expect("sign prehash");
    label_data.sig = Some(sig.to_bytes().to_vec());
    label_data.into()
}

/// Serialize a set of labels into the JSON body of a `com.atproto.label.queryLabels`
/// response, using atrium-api types so the on-wire sig encoding matches what the
/// real HTTP stage will deserialize.
fn labels_response_body(labels: Vec<Label>) -> Vec<u8> {
    let output: Output = OutputData {
        cursor: None,
        labels,
    }
    .into();
    serde_json::to_vec(&output).expect("serialize labels output")
}

/// Build a did:plc did.json document for `did` whose `#atproto_label` verification
/// method uses `multikey` as its `publicKeyMultibase`, and whose labeler/PDS service
/// endpoints point at `labeler_endpoint` and `pds_endpoint` respectively.
fn plc_did_json_with_label_key(
    did: &str,
    multikey: &str,
    labeler_endpoint: &str,
    pds_endpoint: &str,
) -> Vec<u8> {
    let doc = serde_json::json!({
        "@context": [
            "https://www.w3.org/ns/did/v1",
            "https://w3id.org/security/suites/ecdsa-2019/v1"
        ],
        "id": did,
        "alsoKnownAs": [format!("at://{did}")],
        "verificationMethod": [
            {
                "id": "#atproto_label",
                "type": "Multikey",
                "controller": did,
                "publicKeyMultibase": multikey,
            }
        ],
        "service": [
            {
                "id": "#atproto_labeler",
                "type": "AtprotoLabeler",
                "serviceEndpoint": labeler_endpoint,
            },
            {
                "id": "#atproto_pds",
                "type": "AtprotoPersonalDataServer",
                "serviceEndpoint": pds_endpoint,
            }
        ]
    });
    serde_json::to_vec(&doc).expect("serialize did.json")
}

/// Build a sample `LabelData` for `did` with the placeholder sig cleared. The
/// caller is expected to sign it via `sign_label_with_key`.
fn sample_label_data(did: &str, val: &str) -> LabelData {
    LabelData {
        cid: None,
        cts: Datetime::new("2026-01-01T00:00:00.000Z".parse().expect("valid datetime")),
        exp: None,
        neg: Some(false),
        sig: Some(vec![0u8; 64]),
        src: did.parse().expect("valid did"),
        uri: format!("at://{did}/app.bsky.feed.post/abc1"),
        val: val.to_string(),
        ver: Some(1),
    }
}

/// Response variant for FakeHttpClient.
#[derive(Clone)]
enum FakeHttpResponse {
    /// HTTP response with status code and body.
    Http(u16, Vec<u8>),
    /// Transport-level failure (cannot reach the endpoint at all).
    Transport,
}

/// Type alias for the response map in FakeHttpClient.
type FakeHttpResponses = Arc<Mutex<HashMap<String, FakeHttpResponse>>>;

/// Fake HTTP client for testing, seeded with fixture responses.
struct FakeHttpClient {
    responses: FakeHttpResponses,
}

impl FakeHttpClient {
    fn new() -> Self {
        Self {
            responses: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    fn add_response(&self, url: impl Into<String>, status: u16, body: Vec<u8>) {
        self.responses
            .lock()
            .unwrap()
            .insert(url.into(), FakeHttpResponse::Http(status, body));
    }

    /// Register a URL that, when fetched, surfaces as a transport error. The crypto
    /// stage maps any error from `plc_history_for_fragment` to a
    /// `CheckStatus::NetworkError`, so this lets tests exercise the path that
    /// distinguishes "PLC is unreachable" from "labeler signatures are invalid".
    fn add_transport_error(&self, url: impl Into<String>) {
        self.responses
            .lock()
            .unwrap()
            .insert(url.into(), FakeHttpResponse::Transport);
    }
}

#[async_trait]
impl HttpClient for FakeHttpClient {
    async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError> {
        let url_str = url.as_str();
        match self.responses.lock().unwrap().get(url_str).cloned() {
            Some(FakeHttpResponse::Http(status, body)) => Ok((status, body)),
            Some(FakeHttpResponse::Transport) => Err(IdentityError::DidResolutionFailed {
                status: 0,
                body: "Transport error".to_string(),
            }),
            None => Err(IdentityError::DidResolutionFailed {
                status: 404,
                body: "Not found".to_string(),
            }),
        }
    }
}

/// Type alias for the DNS records map in FakeDnsResolver.
type FakeDnsRecords = Arc<Mutex<HashMap<String, Vec<String>>>>;

/// Fake DNS resolver for testing, seeded with fixture records.
struct FakeDnsResolver {
    records: FakeDnsRecords,
}

impl FakeDnsResolver {
    fn new() -> Self {
        Self {
            records: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    #[expect(dead_code)]
    fn add_record(&self, name: impl Into<String>, records: Vec<String>) {
        self.records.lock().unwrap().insert(name.into(), records);
    }
}

#[async_trait]
impl DnsResolver for FakeDnsResolver {
    async fn txt_lookup(&self, name: &str) -> Result<Vec<String>, IdentityError> {
        self.records
            .lock()
            .unwrap()
            .get(name)
            .cloned()
            .ok_or_else(|| IdentityError::DnsLookupFailed {
                source: Box::new(IdentityError::InvalidHandle),
            })
    }
}

/// Helper to render a report to a string.
fn render_report_to_string(
    report: &atproto_devtool::commands::test::labeler::report::LabelerReport,
) -> String {
    let mut buf = Vec::new();
    report
        .render(
            &mut buf,
            &atproto_devtool::commands::test::labeler::report::RenderConfig { no_color: true },
        )
        .expect("render failed");
    String::from_utf8(buf).expect("invalid utf-8")
}

/// Helper to normalize timing in rendered reports for snapshot testing.
fn normalize_timing(rendered: String) -> String {
    // Replace patterns like "elapsed: 1ms" with "elapsed: Xms" for consistent snapshots.
    let mut result = rendered.clone();
    let start = result.find("elapsed: ");
    if let Some(pos) = start {
        let remaining = &result[pos + 9..];
        if let Some(end) = remaining.find("ms") {
            result.replace_range(pos + 9..pos + 9 + end, "X");
        }
    }
    result
}

#[tokio::test]
async fn all_pass_exits_zero_and_renders_all_ok() {
    // Fully healthy labeler with a real ECDSA-signed label whose
    // signature actually verifies against the did:plc atproto_label key.
    let did = "did:plc:test123456789abcdefghijklmnop";
    let (signing_key, multikey) = deterministic_k256_multikey([0x11; 32]);
    let did_json = plc_did_json_with_label_key(
        did,
        &multikey,
        "https://labeler.example.com",
        "https://pds.example.com",
    );
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.json");

    // Produce one real signed label and serialize through atrium-api so the sig
    // encoding matches what the HTTP stage will round-trip on deserialize.
    let signed = sign_label_with_key(sample_label_data(did, "spam"), &signing_key);
    let labels_response = labels_response_body(vec![signed]);

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json,
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );

    let target = parse_target(did, None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, labels_response);
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!("all_pass_exits_zero_and_renders_all_ok", normalized);
    assert_eq!(report.exit_code(), 0, "Expected exit code 0");
}

#[tokio::test]
async fn identity_only_failure_exits_one_with_severity_breakdown() {
    // DID document is missing the labeler signing key.
    let did_json = serde_json::json!({
        "@context": [
            "https://www.w3.org/ns/did/v1",
            "https://w3id.org/security/suites/ecdsa-2019/v1"
        ],
        "id": "did:plc:test123456789abcdefghijklmnop",
        "verificationMethod": [], // Missing atproto_label key
        "service": [
            {
                "id": "#atproto_labeler",
                "type": "AtprotoLabeler",
                "serviceEndpoint": "https://labeler.example.com"
            },
            {
                "id": "#atproto_pds",
                "type": "AtprotoPersonalDataServer",
                "serviceEndpoint": "https://pds.example.com"
            }
        ]
    });

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        serde_json::to_vec(&did_json).unwrap(),
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!(
        "identity_only_failure_exits_one_with_severity_breakdown",
        normalized
    );
    assert_eq!(report.exit_code(), 1, "Expected exit code 1");
}

#[tokio::test]
async fn http_decode_failure_exits_one() {
    // Malformed queryLabels response.
    let did_json = include_bytes!("fixtures/labeler/identity/healthy_plc/did.json");
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.json");

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json.to_vec(),
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, b"{malformed json".to_vec());
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!("http_decode_failure_exits_one", normalized);
    assert_eq!(report.exit_code(), 1, "Expected exit code 1");
}

#[tokio::test]
async fn subscription_transport_error_exits_two() {
    // Subscription stage transport error → network error → exit 2.
    let did_json = include_bytes!("fixtures/labeler/identity/healthy_plc/did.json");
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.json");

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json.to_vec(),
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, br#"{"cursor": null, "labels": []}"#.to_vec());

    let fake_ws = common::FakeWebSocketClient::new();
    // Add a script with a transport error to simulate subscription failure.
    fake_ws.add_script(common::FakeScript {
        frames: vec![],
        inter_frame_delay: Duration::from_millis(0),
        final_wait: None,
        transport_error: true,
        mid_stream_error: false,
    });

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!("subscription_transport_error_exits_two", normalized);
    assert_eq!(
        report.exit_code(),
        2,
        "Expected exit code 2 for network-only error"
    );
}

#[tokio::test]
async fn current_key_fail_history_pass_exits_zero_with_advisory() {
    // did:plc labeler signed with a rotated-out key. The current key in did.json fails
    // verification, but the crypto stage fetches the PLC audit log, finds the historic
    // key, and verifies against it — producing an advisory and exit 0.
    let did = "did:plc:test123456789abcdefghijklmnop";
    let (_current_signing_key, current_multikey) = deterministic_k256_multikey([0x22; 32]);
    let (historic_signing_key, historic_multikey) = deterministic_k256_multikey([0x33; 32]);

    // did.json advertises the current key (the one we will NOT sign with).
    let did_json = plc_did_json_with_label_key(
        did,
        &current_multikey,
        "https://labeler.example.com",
        "https://pds.example.com",
    );
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.json");

    // Sign the label with the historic key so verification against the current key fails
    // and the PLC history fallback path is the only way to succeed.
    let signed = sign_label_with_key(sample_label_data(did, "spam"), &historic_signing_key);
    let labels_response = labels_response_body(vec![signed]);

    // PLC audit log advertises the historic key for the atproto_label fragment.
    let plc_audit_log = serde_json::json!([
        {
            "did": did,
            "cid": "bafyreihxxxxxxxxx1",
            "nullified": false,
            "createdAt": "2026-02-01T00:00:00.000Z",
            "operation": {
                "type": "plc_operation",
                "verificationMethods": {
                    "atproto_label": historic_multikey,
                }
            }
        }
    ]);

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json,
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop/log/audit",
        200,
        serde_json::to_vec(&plc_audit_log).expect("serialize plc audit log"),
    );

    let target = parse_target(did, None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, labels_response);
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!(
        "current_key_fail_history_pass_exits_zero_with_advisory",
        normalized
    );
    assert_eq!(report.exit_code(), 0, "Expected exit code 0");
}

#[tokio::test]
async fn current_key_fail_history_also_fail_exits_one() {
    // did:plc labeler where neither current nor historic key matches. Uses three runtime
    // keypairs: the `current` and `historic` keys advertised in the did.json and PLC
    // audit log respectively, plus a `rogue` key that actually signs the label. The
    // crypto stage must try both advertised keys, fail both, and emit a
    // MultiKeyVerificationFailed diagnostic listing both tried key ids.
    let did = "did:plc:test123456789abcdefghijklmnop";
    let (_current_signing_key, current_multikey) = deterministic_k256_multikey([0x22; 32]);
    let (_historic_signing_key, historic_multikey) = deterministic_k256_multikey([0x33; 32]);
    let (rogue_signing_key, _rogue_multikey) = deterministic_k256_multikey([0x44; 32]);

    let did_json = plc_did_json_with_label_key(
        did,
        &current_multikey,
        "https://labeler.example.com",
        "https://pds.example.com",
    );
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.json");

    let signed = sign_label_with_key(sample_label_data(did, "spam"), &rogue_signing_key);
    let labels_response = labels_response_body(vec![signed]);

    let plc_audit_log = serde_json::json!([
        {
            "did": did,
            "cid": "bafyreihxxxxxxxxx1",
            "nullified": false,
            "createdAt": "2026-02-01T00:00:00.000Z",
            "operation": {
                "type": "plc_operation",
                "verificationMethods": {
                    "atproto_label": historic_multikey,
                }
            }
        }
    ]);

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json,
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop/log/audit",
        200,
        serde_json::to_vec(&plc_audit_log).unwrap(),
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, labels_response);
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!("current_key_fail_history_also_fail_exits_one", normalized);
    assert_eq!(report.exit_code(), 1, "Expected exit code 1");
}

#[tokio::test]
async fn did_web_current_key_fail_exits_one() {
    // did:web labeler where current key fails and no rotation history is available. Uses two runtime
    // keypairs: `current` is advertised in the did.json; `rogue` signs the label.
    // Since did:web has no PLC audit log to fall back on, the crypto stage must
    // emit DidWebNoRotationHistory after the current-key verification fails.
    let did = "did:web:web-labeler.example";
    let (_current_signing_key, current_multikey) = deterministic_k256_multikey([0x55; 32]);
    let (rogue_signing_key, _rogue_multikey) = deterministic_k256_multikey([0x66; 32]);

    let did_json = serde_json::to_vec(&serde_json::json!({
        "@context": [
            "https://www.w3.org/ns/did/v1",
            "https://w3id.org/security/suites/ecdsa-2019/v1"
        ],
        "id": did,
        "verificationMethod": [
            {
                "id": "#atproto_label",
                "type": "Multikey",
                "controller": did,
                "publicKeyMultibase": current_multikey,
            }
        ],
        "service": [
            {
                "id": "#atproto_labeler",
                "type": "AtprotoLabeler",
                "serviceEndpoint": "https://web-labeler.example"
            },
            {
                "id": "#atproto_pds",
                "type": "AtprotoPersonalDataServer",
                "serviceEndpoint": "https://pds.example.com"
            }
        ]
    }))
    .expect("serialize did:web did.json");
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_web/labeler_record.json");

    let signed = sign_label_with_key(sample_label_data(did, "spam"), &rogue_signing_key);
    let labels_response = labels_response_body(vec![signed]);

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://web-labeler.example/.well-known/did.json",
        200,
        did_json,
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:web:web-labeler.example&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );

    let target = parse_target("did:web:web-labeler.example", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, labels_response);
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!("did_web_current_key_fail_exits_one", normalized);
    assert_eq!(report.exit_code(), 1, "Expected exit code 1");
}

#[tokio::test]
async fn canonicalization_error_distinct_diagnostic_code() {
    // Label missing sig field causes canonicalization failure.
    let did_json = include_bytes!("fixtures/labeler/identity/healthy_plc/did.json");
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.json");
    // Label without sig field will fail canonicalization.
    let labels_response = br#"{"cursor":null,"labels":[{"ver":1,"src":"did:plc:test123456789abcdefghijklmnop","uri":"at://did:plc:test123456789abcdefghijklmnop/app.bsky.feed.post/abc1","val":"spam","neg":false,"cts":"2026-01-01T00:00:00.000Z"}]}"#.to_vec();

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json.to_vec(),
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, labels_response);
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!(
        "canonicalization_error_distinct_diagnostic_code",
        normalized
    );
    // Label with missing sig will fail canonicalization, causing exit code 1.
    assert_eq!(report.exit_code(), 1, "Expected exit code 1");
}

#[tokio::test]
async fn plc_directory_unreachable_network_error_no_false_fail() {
    // PLC audit log fetch fails with a transport error during history fallback.
    // The crypto stage must surface the PLC fetch failure as a network error check
    // (not a spec violation), so operators can distinguish an unreachable PLC
    // directory from a genuine labeler signature problem.
    //
    // Setup: advertise a current key in did.json, sign the label with a different
    // runtime key so current-key verification fails, then configure the PLC audit
    // URL to return a transport error. The crypto stage should emit
    // PLC_HISTORY_FETCH with status NetworkError.
    let did = "did:plc:test123456789abcdefghijklmnop";
    let (_current_signing_key, current_multikey) = deterministic_k256_multikey([0x77; 32]);
    let (rogue_signing_key, _rogue_multikey) = deterministic_k256_multikey([0x88; 32]);

    let did_json = plc_did_json_with_label_key(
        did,
        &current_multikey,
        "https://labeler.example.com",
        "https://pds.example.com",
    );
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.json");

    let signed = sign_label_with_key(sample_label_data(did, "spam"), &rogue_signing_key);
    let labels_response = labels_response_body(vec![signed]);

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json,
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );
    http.add_transport_error(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop/log/audit",
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, labels_response);
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!(
        "plc_directory_unreachable_network_error_no_false_fail",
        normalized
    );
    assert_eq!(report.exit_code(), 1, "Expected exit code 1");
}

#[tokio::test]
async fn empty_labeler_skipped_crypto_only_advisory_exits_zero() {
    // Empty labeler (no published labels) produces Skipped crypto and exits 0.
    let did_json = include_bytes!("fixtures/labeler/identity/healthy_plc/did.json");
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.json");

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json.to_vec(),
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, br#"{"cursor": null, "labels": []}"#.to_vec());
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!(
        "empty_labeler_skipped_crypto_only_advisory_exits_zero",
        normalized
    );
    assert_eq!(report.exit_code(), 0, "Expected exit code 0");
}

#[tokio::test]
async fn exit_code_summary_for_network_only_run_is_two() {
    // Network errors only (no spec violations) → exit 2. The labeler is
    // not necessarily out of spec, but the run could not exercise it
    // fully, so the operator should investigate. Spec violations still
    // trump network errors and surface as exit 1.
    let did_json = include_bytes!("fixtures/labeler/identity/healthy_plc/did.json");
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.json");

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();
    // Provide DID response to pass identity stage.
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json.to_vec(),
    );
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json.to_vec(),
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.set_transport_error(); // Force HTTP stage transport error.
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!("exit_code_summary_for_network_only_run_is_two", normalized);
    // Network-only errors (not spec violations) exit 2.
    assert_eq!(
        report.exit_code(),
        2,
        "Expected exit code 2 for network-only errors"
    );
}

#[tokio::test]
async fn skipped_reasons_rendered() {
    // Skipped checks render their reason strings in the report.
    // Use endpoint-only mode (no DID) to trigger skips.
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    let target = parse_target("https://labeler.example.com", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, br#"{"cursor": null, "labels": []}"#.to_vec());
    let fake_ws = common::FakeWebSocketClient::empty();

    let opts = LabelerOptions {
        http: &http,
        dns: &dns,
        http_tee: HttpTee::Test(&fake_tee),
        ws_client: Some(&fake_ws),
        subscribe_timeout: Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;
    let rendered = render_report_to_string(&report);
    let normalized = normalize_timing(rendered);

    insta::assert_snapshot!("skipped_reasons_rendered", normalized);
    // Verify [SKIP] lines with reasons appear.
    assert!(
        normalized.contains("[SKIP]"),
        "Expected SKIP marker in output"
    );
}
