use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

use rustfs_filemeta::{FileInfo, FileInfoOpts, get_file_info};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct RequestRecord {
    encryption: String,
    headers: HashMap<String, String>,
    multipart: bool,
    size_bytes: u64,
    object: String,
}

#[derive(Debug, Deserialize)]
struct HeadRecord {
    #[serde(rename = "ContentLength")]
    content_length: u64,
    #[serde(rename = "ServerSideEncryption")]
    server_side_encryption: Option<String>,
    #[serde(rename = "SSECustomerAlgorithm")]
    sse_customer_algorithm: Option<String>,
    #[serde(rename = "SSECustomerKeyMD5")]
    sse_customer_key_md5: Option<String>,
    #[serde(rename = "SSEKMSKeyId")]
    sse_kms_key_id: Option<String>,
}

#[derive(Debug, Deserialize)]
struct ManifestRecord {
    capture: Option<CaptureRecord>,
}

#[derive(Debug, Deserialize)]
struct CaptureRecord {
    kms_key_id: Option<String>,
}

fn fixture_root() -> PathBuf {
    std::env::var_os("RUSTFS_MINIO_FIXTURE_ROOT")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/minio-generated"))
}

fn case_dir(case_id: &str) -> PathBuf {
    fixture_root().join("cases").join(case_id)
}

fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> T {
    let text = fs::read_to_string(path).unwrap_or_else(|err| panic!("read {}: {err}", path.display()));
    serde_json::from_str(&text).unwrap_or_else(|err| panic!("parse {}: {err}", path.display()))
}

fn find_object_xl_meta(case_dir: &Path) -> PathBuf {
    let backend = case_dir.join("backend");
    for entry in walkdir::WalkDir::new(&backend) {
        let entry = entry.unwrap_or_else(|err| panic!("walk {}: {err}", backend.display()));
        if !entry.file_type().is_file() {
            continue;
        }
        if entry.file_name() != "xl.meta" {
            continue;
        }
        let path = entry.path();
        let path_text = path.to_string_lossy();
        if path_text.contains(".minio.sys") {
            continue;
        }
        return path.to_path_buf();
    }
    panic!("object xl.meta not found under {}", backend.display());
}

fn load_file_info(case_id: &str) -> FileInfo {
    let case = case_dir(case_id);
    let xl_meta = find_object_xl_meta(&case);
    let data = fs::read(&xl_meta).unwrap_or_else(|err| panic!("read {}: {err}", xl_meta.display()));
    get_file_info(
        &data,
        "fixture-bucket",
        "fixture-object",
        "",
        FileInfoOpts {
            data: false,
            include_free_versions: true,
            include_part_checksums: true,
        },
    )
    .unwrap_or_else(|err| panic!("decode {}: {err}", xl_meta.display()))
}

fn read_manifest(case_id: &str) -> ManifestRecord {
    read_json(&case_dir(case_id).join("manifest.json"))
}

fn require_generated_fixture_root() {
    let root = fixture_root();
    assert!(
        root.join("cases").is_dir(),
        "generated fixture root missing: {}. Run test/minio_fixture_lab/lab.py capture-matrix first.",
        root.display()
    );
}

fn metadata_keys(fi: &FileInfo) -> HashMap<&str, &str> {
    fi.metadata.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
}

fn expected_fixture_kms_key_id(case_id: &str, request: &RequestRecord) -> Option<String> {
    request
        .headers
        .get("x-amz-server-side-encryption-aws-kms-key-id")
        .cloned()
        .or_else(|| read_manifest(case_id).capture.and_then(|capture| capture.kms_key_id))
}

#[test]
#[ignore = "requires locally generated MinIO fixtures"]
fn parses_singlepart_sse_s3_fixture() {
    require_generated_fixture_root();

    let request: RequestRecord = read_json(&case_dir("sse-s3-singlepart-64k").join("request.json"));
    let head: HeadRecord = read_json(&case_dir("sse-s3-singlepart-64k").join("head.json"));
    let fi = load_file_info("sse-s3-singlepart-64k");
    let metadata = metadata_keys(&fi);

    assert_eq!(request.encryption, "SSE-S3");
    assert!(!request.multipart);
    assert_eq!(request.object, "dir/object.bin");
    assert_eq!(head.content_length, request.size_bytes);
    assert_eq!(head.server_side_encryption.as_deref(), Some("AES256"));
    assert_eq!(fi.parts.len(), 1);
    assert_eq!(fi.parts[0].actual_size as u64, request.size_bytes);
    assert!(metadata.contains_key("X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key"));
    assert_eq!(metadata.get("content-type"), Some(&"binary/octet-stream"));
}

#[test]
#[ignore = "requires locally generated MinIO fixtures"]
fn parses_singlepart_sse_kms_fixture() {
    require_generated_fixture_root();

    let case_id = "sse-kms-singlepart-64k";
    let request: RequestRecord = read_json(&case_dir(case_id).join("request.json"));
    let head: HeadRecord = read_json(&case_dir(case_id).join("head.json"));
    let fi = load_file_info(case_id);
    let metadata = metadata_keys(&fi);
    let expected_key_id = expected_fixture_kms_key_id(case_id, &request).expect("fixture kms key id");

    assert_eq!(request.encryption, "SSE-KMS");
    assert!(!request.multipart);
    assert_eq!(head.content_length, request.size_bytes);
    assert_eq!(head.server_side_encryption.as_deref(), Some("aws:kms"));
    assert_eq!(request.headers.get("x-amz-server-side-encryption"), Some(&"aws:kms".to_string()));
    assert_eq!(request.headers.get("x-amz-server-side-encryption-aws-kms-key-id"), Some(&expected_key_id));
    assert_eq!(head.sse_kms_key_id.as_deref(), Some(format!("arn:aws:kms:{expected_key_id}").as_str()));
    assert_eq!(fi.parts.len(), 1);
    assert_eq!(fi.parts[0].actual_size as u64, request.size_bytes);
    assert_eq!(
        metadata.get("X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id"),
        Some(&expected_key_id.as_str())
    );
    assert_eq!(
        metadata.get("X-Minio-Internal-Server-Side-Encryption-Context").copied(),
        request
            .headers
            .get("x-amz-server-side-encryption-context")
            .map(String::as_str)
    );
    assert!(metadata.contains_key("X-Minio-Internal-Server-Side-Encryption-Kms-Sealed-Key"));
}

#[test]
#[ignore = "requires locally generated MinIO fixtures"]
fn parses_multipart_sse_s3_fixture() {
    require_generated_fixture_root();

    let case_id = "sse-s3-multipart-8m";
    let request: RequestRecord = read_json(&case_dir(case_id).join("request.json"));
    let head: HeadRecord = read_json(&case_dir(case_id).join("head.json"));
    let fi = load_file_info(case_id);
    let metadata = metadata_keys(&fi);

    assert_eq!(request.encryption, "SSE-S3");
    assert!(request.multipart);
    assert_eq!(head.content_length, request.size_bytes);
    assert_eq!(head.server_side_encryption.as_deref(), Some("AES256"));
    assert_eq!(fi.parts.len(), 2);
    assert_eq!(fi.parts.iter().map(|part| part.actual_size as u64).sum::<u64>(), request.size_bytes);
    assert!(
        metadata
            .get("X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id")
            .is_some_and(|value| !value.is_empty())
    );
    assert!(metadata.contains_key("X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key"));
    assert!(metadata.contains_key("X-Minio-Internal-Encrypted-Multipart"));
    assert_eq!(metadata.get("X-Minio-Internal-actual-size"), Some(&"8388608"));
}

#[test]
#[ignore = "requires locally generated MinIO fixtures"]
fn parses_multipart_sse_kms_fixture() {
    require_generated_fixture_root();

    let case_id = "sse-kms-multipart-8m";
    let request: RequestRecord = read_json(&case_dir(case_id).join("request.json"));
    let head: HeadRecord = read_json(&case_dir(case_id).join("head.json"));
    let fi = load_file_info(case_id);
    let metadata = metadata_keys(&fi);
    let expected_key_id = expected_fixture_kms_key_id(case_id, &request).expect("fixture kms key id");

    assert_eq!(request.encryption, "SSE-KMS");
    assert!(request.multipart);
    assert_eq!(head.content_length, request.size_bytes);
    assert_eq!(head.server_side_encryption.as_deref(), Some("aws:kms"));
    assert_eq!(request.headers.get("x-amz-server-side-encryption-aws-kms-key-id"), Some(&expected_key_id));
    assert_eq!(head.sse_kms_key_id.as_deref(), Some(format!("arn:aws:kms:{expected_key_id}").as_str()));
    assert_eq!(fi.parts.len(), 2);
    assert_eq!(fi.parts.iter().map(|part| part.actual_size as u64).sum::<u64>(), request.size_bytes);
    assert_eq!(
        metadata.get("X-Minio-Internal-Server-Side-Encryption-Context").copied(),
        request
            .headers
            .get("x-amz-server-side-encryption-context")
            .map(String::as_str)
    );
    assert!(metadata.contains_key("X-Minio-Internal-Encrypted-Multipart"));
    assert_eq!(metadata.get("X-Minio-Internal-actual-size"), Some(&"8388608"));
}

#[test]
#[ignore = "requires locally generated MinIO fixtures"]
fn parses_singlepart_sse_c_fixture() {
    require_generated_fixture_root();

    let case_id = "sse-c-singlepart-64k";
    let request: RequestRecord = read_json(&case_dir(case_id).join("request.json"));
    let head: HeadRecord = read_json(&case_dir(case_id).join("head.json"));
    let fi = load_file_info(case_id);
    let metadata = metadata_keys(&fi);

    assert_eq!(request.encryption, "SSE-C");
    assert!(!request.multipart);
    assert_eq!(head.content_length, request.size_bytes);
    assert_eq!(head.sse_customer_algorithm.as_deref(), Some("AES256"));
    assert_eq!(
        head.sse_customer_key_md5.as_deref(),
        request
            .headers
            .get("x-amz-server-side-encryption-customer-key-md5")
            .map(String::as_str)
    );
    assert_eq!(fi.parts.len(), 1);
    assert_eq!(fi.parts[0].actual_size as u64, request.size_bytes);
    assert!(metadata.contains_key("X-Minio-Internal-Server-Side-Encryption-Sealed-Key"));
    assert!(!metadata.contains_key("X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id"));
}

#[test]
#[ignore = "requires locally generated MinIO fixtures"]
fn parses_multipart_sse_c_fixture() {
    require_generated_fixture_root();

    let case_id = "sse-c-multipart-8m";
    let request: RequestRecord = read_json(&case_dir(case_id).join("request.json"));
    let head: HeadRecord = read_json(&case_dir(case_id).join("head.json"));
    let fi = load_file_info(case_id);
    let metadata = metadata_keys(&fi);

    assert_eq!(request.encryption, "SSE-C");
    assert!(request.multipart);
    assert_eq!(head.content_length, request.size_bytes);
    assert_eq!(head.sse_customer_algorithm.as_deref(), Some("AES256"));
    assert_eq!(
        head.sse_customer_key_md5.as_deref(),
        request
            .headers
            .get("x-amz-server-side-encryption-customer-key-md5")
            .map(String::as_str)
    );
    assert_eq!(fi.parts.len(), 2);
    assert_eq!(fi.parts.iter().map(|part| part.actual_size as u64).sum::<u64>(), request.size_bytes);
    assert!(metadata.contains_key("X-Minio-Internal-Server-Side-Encryption-Sealed-Key"));
    assert!(!metadata.contains_key("X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id"));
    assert!(metadata.contains_key("X-Minio-Internal-Encrypted-Multipart"));
    assert_eq!(metadata.get("X-Minio-Internal-actual-size"), Some(&"8388608"));
}
