#![expect(clippy::unwrap_used)]

use super::*;
use crate::test_helpers::make_attachment;
use crate::types::{Attachment, OutputFormat};

fn output_attachment(id: u64, summary: &str) -> Attachment {
    make_attachment(id, 42, &format!("file_{id}.patch"), summary, None)
}

fn capture(format: OutputFormat, attachments: &[Attachment]) -> String {
    let mut buf = Vec::new();
    write_attachments(attachments, format, &mut buf);
    String::from_utf8(buf).unwrap()
}

fn capture_batch(format: OutputFormat, result: &AttachmentBatchResult) -> (String, String) {
    let mut out = Vec::new();
    let mut err = Vec::new();
    write_attachment_batch(result, format, &mut out, &mut err);
    (
        String::from_utf8(out).unwrap(),
        String::from_utf8(err).unwrap(),
    )
}

#[test]
fn write_attachments_json_empty() {
    let attachments: Vec<Attachment> = vec![];
    let json = serde_json::to_string_pretty(&attachments).unwrap();
    assert_eq!(json, "[]");
}

#[test]
fn write_attachments_json_one_attachment() {
    let attachments = vec![output_attachment(10, "Fix patch")];
    let json = serde_json::to_string_pretty(&attachments).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed[0]["id"], 10);
    assert_eq!(parsed[0]["summary"], "Fix patch");
    assert_eq!(parsed[0]["file_name"], "file_10.patch");
    assert_eq!(parsed[0]["content_type"], "text/plain");
    assert_eq!(parsed[0]["size"], 1234);
}

#[test]
fn attachment_text_format_fields() {
    let att = output_attachment(10, "Fix patch");
    assert_eq!(att.file_name, "file_10.patch");
    assert_eq!(att.content_type, "text/plain");
    assert_eq!(att.size, 1234);
    assert_eq!(att.creator.as_deref(), Some("author@example.com"));
    assert!(!att.is_obsolete);
    assert!(!att.is_private);
}

#[test]
fn write_attachments_json_obsolete_and_private() {
    let mut att = output_attachment(11, "Old patch");
    att.is_obsolete = true;
    att.is_private = true;
    let json = serde_json::to_string(&att).unwrap();
    let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
    assert_eq!(parsed["is_obsolete"], true);
    assert_eq!(parsed["is_private"], true);
}

#[test]
fn write_attachments_table_empty_says_no_attachments() {
    let output = capture(OutputFormat::Table, &[]);
    assert!(output.contains("No attachments."));
}

#[test]
fn write_attachments_json_empty_renders_empty_array() {
    let output = capture(OutputFormat::Json, &[]);
    let parsed: serde_json::Value = serde_json::from_str(output.trim()).unwrap();
    assert!(parsed.is_array());
    assert_eq!(parsed.as_array().unwrap().len(), 0);
}

#[test]
fn write_attachments_table_renders_all_fields() {
    let mut att = output_attachment(7, "Helpful patch");
    att.is_obsolete = true;
    att.is_private = true;
    let output = capture(OutputFormat::Table, &[att]);
    assert!(output.contains("Attachment"));
    assert!(output.contains("#7"));
    assert!(output.contains("Helpful patch"));
    assert!(output.contains("[OBSOLETE]"));
    assert!(output.contains("[PRIVATE]"));
    assert!(output.contains("file_7.patch"));
    assert!(output.contains("text/plain"));
    assert!(output.contains("1234 bytes"));
    assert!(output.contains("Creator"));
    assert!(output.contains("author@example.com"));
    assert!(output.contains("Created"));
    assert!(output.contains("2025-03-01T09:00:00Z"));
}

#[test]
fn write_attachments_table_missing_optional_fields_render_dash() {
    let attachments = vec![Attachment {
        id: 8,
        bug_id: 42,
        file_name: "patché.txt".into(),
        summary: "Unicode summary — é".into(),
        content_type: "text/plain".into(),
        creator: None,
        creation_time: None,
        last_change_time: None,
        size: 0,
        is_obsolete: false,
        is_private: false,
        is_patch: false,
        data: None,
    }];
    let output = capture(OutputFormat::Table, &attachments);
    assert!(output.contains("Unicode summary — é"));
    assert!(output.contains("patché.txt"));
    assert!(
        output.contains("Creator       -"),
        "expected dashed Creator field, got: {output}"
    );
    assert!(
        output.contains("Created       -"),
        "expected dashed Created field, got: {output}"
    );
    assert!(!output.contains("[OBSOLETE]"));
    assert!(!output.contains("[PRIVATE]"));
}

#[test]
fn write_attachments_table_renders_patch_tag() {
    let mut att = output_attachment(12, "Patch attachment");
    att.is_patch = true;
    let output = capture(OutputFormat::Table, &[att]);
    assert!(output.contains("Attachment"));
    assert!(output.contains("#12"));
    assert!(output.contains("Patch attachment"));
    assert!(output.contains("[PATCH]"));
    assert!(!output.contains("[OBSOLETE]"));
    assert!(!output.contains("[PRIVATE]"));
}

#[test]
fn write_attachments_table_patch_tag_precedes_obsolete_and_private() {
    let mut att = output_attachment(13, "All flags");
    att.is_patch = true;
    att.is_obsolete = true;
    att.is_private = true;
    let output = capture(OutputFormat::Table, &[att]);
    let patch_idx = output.find("[PATCH]").expect("[PATCH] tag should render");
    let obsolete_idx = output
        .find("[OBSOLETE]")
        .expect("[OBSOLETE] tag should render");
    let private_idx = output
        .find("[PRIVATE]")
        .expect("[PRIVATE] tag should render");
    assert!(patch_idx < obsolete_idx);
    assert!(obsolete_idx < private_idx);
}

#[test]
fn write_attachments_json_one_via_write() {
    let output = capture(OutputFormat::Json, &[output_attachment(99, "Json patch")]);
    let parsed: serde_json::Value = serde_json::from_str(output.trim()).unwrap();
    assert_eq!(parsed[0]["id"], 99);
    assert_eq!(parsed[0]["summary"], "Json patch");
    assert_eq!(parsed[0]["file_name"], "file_99.patch");
}

#[test]
fn write_attachments_does_not_emit_ansi_when_writing_to_buffer() {
    let output = capture(OutputFormat::Table, &[output_attachment(1, "p")]);
    assert!(
        !output.contains('\x1b'),
        "expected no ANSI escapes when writing to Vec<u8>: {output:?}",
    );
}

#[test]
fn attachment_batch_result_json_shape() {
    let result = AttachmentBatchResult {
        out_dir: "./attachments".into(),
        bug_results: vec![BugDownloadResult {
            bug_id: 12345,
            status: TargetStatus::Ok,
            files: vec![DownloadedFile {
                attachment_id: 9876,
                path: "./attachments/12345/9876.patch.diff".into(),
                bytes: 4096,
            }],
            error: None,
        }],
        attachment_results: vec![],
        summary: BatchSummary {
            succeeded: 1,
            failed: 0,
            total_bytes: 4096,
        },
    };

    let json = serde_json::to_value(&result).unwrap();
    assert_eq!(json["out_dir"], "./attachments");
    assert_eq!(json["bug_results"][0]["bug_id"], 12345);
    assert_eq!(json["bug_results"][0]["status"], "ok");
    assert_eq!(json["bug_results"][0]["files"][0]["attachment_id"], 9876);
    assert_eq!(json["bug_results"][0]["files"][0]["bytes"], 4096);
    assert_eq!(json["summary"]["succeeded"], 1);
    assert_eq!(json["summary"]["total_bytes"], 4096);
}

#[test]
fn attachment_batch_result_omits_empty_optional_fields() {
    let result = AttachmentBatchResult {
        out_dir: "./attachments".into(),
        bug_results: vec![BugDownloadResult {
            bug_id: 12345,
            status: TargetStatus::Ok,
            files: vec![],
            error: None,
        }],
        attachment_results: vec![AttachmentDownloadResult {
            attachment_id: 999,
            status: TargetStatus::Error,
            bug_id: None,
            path: None,
            bytes: None,
            error: Some("not found".into()),
        }],
        summary: BatchSummary {
            succeeded: 0,
            failed: 1,
            total_bytes: 0,
        },
    };
    let json = serde_json::to_value(&result).unwrap();
    let bug = &json["bug_results"][0];
    assert!(bug.get("files").is_none(), "files should skip when empty");
    assert!(bug.get("error").is_none(), "error should skip when None");
    let att = &json["attachment_results"][0];
    assert!(att.get("path").is_none());
    assert!(att.get("bytes").is_none());
    assert!(att.get("bug_id").is_none());
    assert_eq!(att["error"], "not found");
}

#[test]
fn target_status_serializes_lowercase() {
    assert_eq!(serde_json::to_string(&TargetStatus::Ok).unwrap(), "\"ok\"");
    assert_eq!(
        serde_json::to_string(&TargetStatus::Error).unwrap(),
        "\"error\"",
    );
}

fn sample_batch_result() -> AttachmentBatchResult {
    AttachmentBatchResult {
        out_dir: "./attachments".into(),
        bug_results: vec![BugDownloadResult {
            bug_id: 12345,
            status: TargetStatus::Ok,
            files: vec![
                DownloadedFile {
                    attachment_id: 9876,
                    path: "./attachments/12345/9876.patch.diff".into(),
                    bytes: 4096,
                },
                DownloadedFile {
                    attachment_id: 9877,
                    path: "./attachments/12345/9877.trace.log".into(),
                    bytes: 2048,
                },
            ],
            error: None,
        }],
        attachment_results: vec![],
        summary: BatchSummary {
            succeeded: 2,
            failed: 0,
            total_bytes: 6144,
        },
    }
}

#[test]
fn write_attachment_batch_table_includes_summary() {
    let result = sample_batch_result();
    let (out, _err) = capture_batch(OutputFormat::Table, &result);
    assert!(out.contains("Bug #12345"), "missing bug header: {out}");
    assert!(
        out.contains("./attachments/12345/9876.patch.diff"),
        "missing file path: {out}",
    );
    assert!(out.contains("2 succeeded"), "missing summary line: {out}");
    assert!(out.contains("6144"), "missing total_bytes: {out}");
}

#[test]
fn write_attachment_batch_json_emits_typed_payload() {
    let result = sample_batch_result();
    let (out, _err) = capture_batch(OutputFormat::Json, &result);
    let parsed: serde_json::Value = serde_json::from_str(out.trim()).unwrap();
    assert_eq!(parsed["summary"]["succeeded"], 2);
    assert_eq!(parsed["bug_results"][0]["files"][0]["attachment_id"], 9876);
}

#[test]
fn write_attachment_batch_table_bug_error_writes_to_err() {
    let result = AttachmentBatchResult {
        out_dir: "./attachments".into(),
        bug_results: vec![BugDownloadResult {
            bug_id: 99999,
            status: TargetStatus::Error,
            files: vec![],
            error: Some("bug not found".into()),
        }],
        attachment_results: vec![],
        summary: BatchSummary {
            succeeded: 0,
            failed: 1,
            total_bytes: 0,
        },
    };
    let mut out = Vec::new();
    let mut err = Vec::new();
    write_attachment_batch_table(&result, &mut out, &mut err);
    let err_str = String::from_utf8(err).unwrap();
    let out_str = String::from_utf8(out).unwrap();
    assert!(
        err_str.contains("Bug #99999: bug not found"),
        "stderr should carry bug-error: {err_str}",
    );
    assert!(out_str.contains("0 succeeded, 1 failed"));
}

#[test]
fn write_attachment_batch_table_bug_error_with_partial_files() {
    let result = AttachmentBatchResult {
        out_dir: "./attachments".into(),
        bug_results: vec![BugDownloadResult {
            bug_id: 12345,
            status: TargetStatus::Error,
            files: vec![DownloadedFile {
                attachment_id: 9876,
                path: "./attachments/12345/9876.a.txt".into(),
                bytes: 5,
            }],
            error: Some("write failed for #9877".into()),
        }],
        attachment_results: vec![],
        summary: BatchSummary {
            succeeded: 1,
            failed: 1,
            total_bytes: 5,
        },
    };
    let mut out = Vec::new();
    let mut err = Vec::new();
    write_attachment_batch_table(&result, &mut out, &mut err);
    let out_str = String::from_utf8(out).unwrap();
    let err_str = String::from_utf8(err).unwrap();
    assert!(
        err_str.contains("Bug #12345: write failed for #9877"),
        "stderr should carry bug-error: {err_str}",
    );
    assert!(
        out_str.contains("./attachments/12345/9876.a.txt") && out_str.contains("[partial]"),
        "stdout should list partial file with [partial] tag: {out_str}",
    );
}

#[test]
fn write_attachment_batch_table_attachment_error_writes_to_err() {
    let result = AttachmentBatchResult {
        out_dir: "./attachments".into(),
        bug_results: vec![],
        attachment_results: vec![AttachmentDownloadResult {
            attachment_id: 4242,
            status: TargetStatus::Error,
            bug_id: None,
            path: None,
            bytes: None,
            error: Some("attachment 4242 not found".into()),
        }],
        summary: BatchSummary {
            succeeded: 0,
            failed: 1,
            total_bytes: 0,
        },
    };
    let mut out = Vec::new();
    let mut err = Vec::new();
    write_attachment_batch_table(&result, &mut out, &mut err);
    let err_str = String::from_utf8(err).unwrap();
    assert!(
        err_str.contains("Attachment #4242: attachment 4242 not found"),
        "stderr should carry attachment-error: {err_str}",
    );
}

#[test]
fn write_attachment_batch_table_attachment_ok_writes_path_and_bytes() {
    let result = AttachmentBatchResult {
        out_dir: "./attachments".into(),
        bug_results: vec![],
        attachment_results: vec![AttachmentDownloadResult {
            attachment_id: 4242,
            status: TargetStatus::Ok,
            bug_id: Some(67890),
            path: Some("./attachments/67890/4242.extra.bin".into()),
            bytes: Some(256),
            error: None,
        }],
        summary: BatchSummary {
            succeeded: 1,
            failed: 0,
            total_bytes: 256,
        },
    };
    let mut out = Vec::new();
    let mut err = Vec::new();
    write_attachment_batch_table(&result, &mut out, &mut err);
    let out_str = String::from_utf8(out).unwrap();
    assert!(
        out_str.contains("Attachment") && out_str.contains("#4242"),
        "stdout should include attachment header: {out_str}",
    );
    assert!(out_str.contains("./attachments/67890/4242.extra.bin"));
    assert!(out_str.contains("256"));
}

#[test]
fn batch_summary_from_results_aggregates_correctly() {
    let bug_results = vec![
        BugDownloadResult {
            bug_id: 1,
            status: TargetStatus::Ok,
            files: vec![
                DownloadedFile {
                    attachment_id: 10,
                    path: "p1".into(),
                    bytes: 100,
                },
                DownloadedFile {
                    attachment_id: 11,
                    path: "p2".into(),
                    bytes: 200,
                },
            ],
            error: None,
        },
        BugDownloadResult {
            bug_id: 2,
            status: TargetStatus::Error,
            files: vec![],
            error: Some("missing".into()),
        },
    ];
    let attachment_results = vec![
        AttachmentDownloadResult {
            attachment_id: 30,
            status: TargetStatus::Ok,
            bug_id: Some(3),
            path: Some("p3".into()),
            bytes: Some(50),
            error: None,
        },
        AttachmentDownloadResult {
            attachment_id: 31,
            status: TargetStatus::Error,
            bug_id: None,
            path: None,
            bytes: None,
            error: Some("missing".into()),
        },
    ];
    let summary = BatchSummary::from_results(&bug_results, &attachment_results);
    assert_eq!(summary.succeeded, 3);
    assert_eq!(summary.failed, 2);
    assert_eq!(summary.total_bytes, 350);
}
