//! Integration tests for the labeler identity stage using snapshot tests.

mod common;

use async_trait::async_trait;
use atproto_devtool::commands::test::labeler::pipeline::{
    HttpTee, LabelerOptions, parse_target, run_pipeline,
};
use atproto_devtool::common::identity::{DnsResolver, HttpClient, IdentityError};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use url::Url;

/// Type alias for the response map in FakeHttpClient.
type FakeHttpResponses = Arc<Mutex<HashMap<String, (u16, Vec<u8>)>>>;

/// 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(), (status, body));
    }
}

#[async_trait]
impl HttpClient for FakeHttpClient {
    async fn get_bytes(&self, url: &Url) -> Result<(u16, Vec<u8>), IdentityError> {
        let url_str = url.as_str();
        self.responses
            .lock()
            .unwrap()
            .get(url_str)
            .cloned()
            .ok_or_else(|| 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())),
        }
    }

    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
}

/// Helper function to return a healthy labels response (non-empty with valid labels).
fn healthy_labels_response() -> Vec<u8> {
    br#"{"cursor":null,"labels":[{"ver":1,"src":"did:plc:healthy","uri":"at://did:plc:healthy/app.bsky.feed.post/abc","val":"test-label","neg":false,"cts":"2026-04-13T00:00:00.000Z"}]}"#.to_vec()
}

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

    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock the DID resolution.
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json,
    );

    // Mock the labeler record fetch.
    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,
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, healthy_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

    let report = run_pipeline(target, opts).await;

    let rendered = normalize_timing(render_report_to_string(&report));
    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn endpoint_only_no_did_skips_identity() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    let target = parse_target("https://example.com/labeler", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec();
    fake_tee.add_response(None, 200, empty_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn handle_resolution_happy_path() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock DNS resolution of handle.
    dns.add_record(
        "_atproto.alice.example",
        vec!["did=did:plc:test123456789abcdefghijklmnop".to_string()],
    );

    // Mock DID resolution.
    let did_json = include_bytes!("fixtures/labeler/identity/healthy_plc/did.json").to_vec();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json,
    );

    // Mock labeler record fetch.
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.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,
    );

    let target = parse_target("alice.example", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec();
    fake_tee.add_response(None, 200, empty_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn did_plc_direct_happy_path() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock DID resolution.
    let did_json = include_bytes!("fixtures/labeler/identity/healthy_plc/did.json").to_vec();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json,
    );

    // Mock labeler record fetch.
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_plc/labeler_record.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,
    );

    let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    fake_tee.add_response(None, 200, healthy_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn did_web_direct_happy_path() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock DID resolution for did:web.
    let did_json = include_bytes!("fixtures/labeler/identity/healthy_web/did.json").to_vec();
    http.add_response(
        "https://web-labeler.example/.well-known/did.json",
        200,
        did_json,
    );

    // Mock labeler record fetch.
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/healthy_web/labeler_record.json").to_vec();
    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,
    );

    let target = parse_target("did:web:web-labeler.example", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec();
    fake_tee.add_response(None, 200, empty_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn plc_directory_unreachable_renders_network_error() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Don't add any response for PLC directory or DNS resolver - causes network error.

    let target = parse_target("alice.test", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec();
    fake_tee.add_response(None, 200, empty_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn missing_labeler_record_renders_404_distinct_from_transport() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock DID resolution with a valid healthy DID doc.
    let did_json = include_bytes!("fixtures/labeler/identity/healthy_plc/did.json").to_vec();
    http.add_response(
        "https://plc.directory/did:plc:test123456789abcdefghijklmnop",
        200,
        did_json,
    );

    // Mock 404 for labeler record (the actual test case).
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123456789abcdefghijklmnop&collection=app.bsky.labeler.service&rkey=self",
        404,
        b"Not found".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, healthy_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn missing_service_renders_spec_violation_with_span() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock DID resolution with missing labeler service.
    let did_json = include_bytes!("fixtures/labeler/identity/missing_service/did.json").to_vec();
    http.add_response(
        "https://plc.directory/did:plc:missing_service_test_123456789",
        200,
        did_json,
    );

    // Mock labeler record fetch (should not be reached).
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/missing_service/labeler_record.json").to_vec();
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:missing_service_test_123456789&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json,
    );

    let target =
        parse_target("did:plc:missing_service_test_123456789", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec();
    fake_tee.add_response(None, 200, empty_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn missing_signing_key_renders_spec_violation() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock DID resolution with missing signing key.
    let did_json =
        include_bytes!("fixtures/labeler/identity/missing_signing_key/did.json").to_vec();
    http.add_response(
        "https://plc.directory/did:plc:missing_signing_key_test_12345",
        200,
        did_json,
    );

    // Mock labeler record fetch (should not be reached).
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/missing_signing_key/labeler_record.json")
            .to_vec();
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:missing_signing_key_test_12345&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json,
    );

    let target =
        parse_target("did:plc:missing_signing_key_test_12345", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec();
    fake_tee.add_response(None, 200, empty_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn non_https_endpoint_renders_spec_violation() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock DID resolution with non-HTTPS endpoint.
    let did_json = include_bytes!("fixtures/labeler/identity/non_https_endpoint/did.json").to_vec();
    http.add_response(
        "https://plc.directory/did:plc:non_https_endpoint_test_123456",
        200,
        did_json,
    );

    // Mock labeler record fetch (should not be reached).
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/non_https_endpoint/labeler_record.json").to_vec();
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:non_https_endpoint_test_123456&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json,
    );

    let target =
        parse_target("did:plc:non_https_endpoint_test_123456", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec();
    fake_tee.add_response(None, 200, empty_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn empty_policies_renders_spec_violation_with_span() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock DID resolution.
    let did_json = include_bytes!("fixtures/labeler/identity/empty_policies/did.json").to_vec();
    http.add_response(
        "https://plc.directory/did:plc:empty_policies_test_123456789ab",
        200,
        did_json,
    );

    // Mock labeler record fetch with empty policies.
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/empty_policies/labeler_record.json").to_vec();
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:empty_policies_test_123456789ab&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json,
    );

    let target =
        parse_target("did:plc:empty_policies_test_123456789ab", None).expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec();
    fake_tee.add_response(None, 200, empty_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}

#[tokio::test]
async fn endpoint_mismatch_spec_violation() {
    let http = FakeHttpClient::new();
    let dns = FakeDnsResolver::new();

    // Mock DID resolution.
    let did_json = include_bytes!("fixtures/labeler/identity/endpoint_mismatch/did.json").to_vec();
    http.add_response(
        "https://plc.directory/did:plc:endpoint_mismatch_test_123456789",
        200,
        did_json,
    );

    // Mock labeler record fetch.
    let labeler_record_json =
        include_bytes!("fixtures/labeler/identity/endpoint_mismatch/labeler_record.json").to_vec();
    http.add_response(
        "https://pds.example.com/xrpc/com.atproto.repo.getRecord?repo=did:plc:endpoint_mismatch_test_123456789&collection=app.bsky.labeler.service&rkey=self",
        200,
        labeler_record_json,
    );

    // The target is the mismatched URL; the DID says a different endpoint.
    let target = parse_target(
        "https://other-labeler.example/",
        Some("did:plc:endpoint_mismatch_test_123456789"),
    )
    .expect("parse failed");
    let fake_tee = common::FakeRawHttpTee::new();
    let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec();
    fake_tee.add_response(None, 200, empty_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: std::time::Duration::from_secs(5),
        verbose: false,
    };

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

    insta::assert_snapshot!(rendered);
}
