use std::{collections::HashMap, io::Write, path::Path};

use flate2::{write::DeflateEncoder, Compression as FlateCompression};
use tempfile::tempdir;
use zipdiv::{variants_catalog, Composition, Engine, ZipError, SPEC_FUNCTIONS};

#[derive(Clone)]
struct Item<'a> {
    name: &'a str,
    bytes: &'a [u8],
    deflate: bool,
}

fn archive(items: &[Item<'_>], comment: &[u8]) -> Vec<u8> {
    let mut out = Vec::new();
    let mut records = Vec::new();
    for item in items {
        let offset = out.len() as u32;
        let encoded = if item.deflate {
            let mut encoder = DeflateEncoder::new(Vec::new(), FlateCompression::default());
            encoder.write_all(item.bytes).unwrap();
            encoder.finish().unwrap()
        } else {
            item.bytes.to_vec()
        };
        let method = if item.deflate { 8u16 } else { 0 };
        let crc = crc32fast::hash(item.bytes);
        push32(&mut out, 0x0403_4b50);
        push16(&mut out, 20);
        push16(&mut out, 1 << 11);
        push16(&mut out, method);
        push16(&mut out, 0);
        push16(&mut out, 0);
        push32(&mut out, crc);
        push32(&mut out, encoded.len() as u32);
        push32(&mut out, item.bytes.len() as u32);
        push16(&mut out, item.name.len() as u16);
        push16(&mut out, 0);
        out.extend(item.name.as_bytes());
        out.extend(&encoded);
        records.push((item.clone(), offset, encoded.len() as u32, crc, method));
    }
    let central_offset = out.len() as u32;
    for (item, offset, compressed, crc, method) in records {
        push32(&mut out, 0x0201_4b50);
        push16(&mut out, 20);
        push16(&mut out, 20);
        push16(&mut out, 1 << 11);
        push16(&mut out, method);
        push16(&mut out, 0);
        push16(&mut out, 0);
        push32(&mut out, crc);
        push32(&mut out, compressed);
        push32(&mut out, item.bytes.len() as u32);
        push16(&mut out, item.name.len() as u16);
        push16(&mut out, 0);
        push16(&mut out, 0);
        push16(&mut out, 0);
        push16(&mut out, 0);
        push32(&mut out, 0);
        push32(&mut out, offset);
        out.extend(item.name.as_bytes());
    }
    let central_size = out.len() as u32 - central_offset;
    push32(&mut out, 0x0605_4b50);
    push16(&mut out, 0);
    push16(&mut out, 0);
    push16(&mut out, items.len() as u16);
    push16(&mut out, items.len() as u16);
    push32(&mut out, central_size);
    push32(&mut out, central_offset);
    push16(&mut out, comment.len() as u16);
    out.extend(comment);
    out
}
fn push16(out: &mut Vec<u8>, value: u16) {
    out.extend(value.to_le_bytes());
}
fn push32(out: &mut Vec<u8>, value: u32) {
    out.extend(value.to_le_bytes());
}

fn representative() -> Vec<u8> {
    archive(
        &[
            Item {
                name: "plain.txt",
                bytes: b"stored payload",
                deflate: false,
            },
            Item {
                name: "nested/deep/data.bin",
                bytes: b"deflated payload deflated payload",
                deflate: true,
            },
            Item {
                name: "empty/",
                bytes: b"",
                deflate: false,
            },
        ],
        b"comment containing PK\x05\x06 is legal",
    )
}

#[test]
fn lists_and_extracts_stored_and_deflated_entries() {
    let bytes = representative();
    let dir = tempdir().unwrap();
    let mut engine = Engine::new(Composition::default()).unwrap();
    let entries = engine.list_bytes(&bytes).unwrap();
    assert_eq!(
        entries.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(),
        ["plain.txt", "nested/deep/data.bin", "empty/"]
    );
    engine.extract_bytes(&bytes, dir.path()).unwrap();
    assert_eq!(
        std::fs::read(dir.path().join("plain.txt")).unwrap(),
        b"stored payload"
    );
    assert_eq!(
        std::fs::read(dir.path().join("nested/deep/data.bin")).unwrap(),
        b"deflated payload deflated payload"
    );
}

#[test]
fn every_non_faulty_variant_satisfies_the_archive_contract() {
    let bytes = representative();
    for variant in variants_catalog().into_iter().filter(|v| !v.experimental) {
        let mut composition = Composition::default();
        composition.implementations.insert(
            variant.function.into(),
            format!("{}.{}", variant.function, variant.variant),
        );
        let dir = tempdir().unwrap();
        let mut engine = Engine::new(composition).unwrap();
        engine
            .extract_bytes(&bytes, dir.path())
            .unwrap_or_else(|e| panic!("{}:{} failed: {e}", variant.function, variant.variant));
        assert_eq!(
            std::fs::read(dir.path().join("plain.txt")).unwrap(),
            b"stored payload"
        );
    }
}

#[test]
fn every_spec_function_has_at_least_five_accepted_implementations() {
    let catalog = variants_catalog();
    for function in SPEC_FUNCTIONS {
        let accepted = catalog
            .iter()
            .filter(|variant| variant.function == *function && !variant.experimental)
            .count();
        assert!(
            accepted >= 5,
            "{function} has only {accepted} accepted implementations"
        );
    }
    assert_eq!(
        catalog
            .iter()
            .filter(|variant| !variant.experimental)
            .count(),
        45
    );
}

#[test]
fn dispatch_history_names_all_selected_implementations() {
    let mut composition = Composition::default();
    for function in SPEC_FUNCTIONS {
        let variant = variants_catalog()
            .into_iter()
            .find(|v| {
                v.function == *function
                    && !v.experimental
                    && v.variant != composition.variant(function)
            })
            .unwrap();
        composition.implementations.insert(
            (*function).into(),
            format!("{}.{}", function, variant.variant),
        );
    }
    let expected = composition.implementations.clone();
    let dir = tempdir().unwrap();
    let mut engine = Engine::new(composition).unwrap();
    engine.extract_bytes(&representative(), dir.path()).unwrap();
    let calls = engine.recent_calls();
    for (function, implementation) in expected {
        assert!(
            calls.contains(&format!("{function}:{implementation}")),
            "missing call for {function}"
        );
    }
}

#[test]
fn rejects_traversal_and_does_not_write_outside_root() {
    let bytes = archive(
        &[Item {
            name: "../escape.txt",
            bytes: b"no",
            deflate: false,
        }],
        b"",
    );
    for normalizer in variants_catalog()
        .into_iter()
        .filter(|variant| variant.function == "normalize_extraction_path" && !variant.experimental)
    {
        let mut composition = Composition::default();
        composition.implementations.insert(
            "normalize_extraction_path".into(),
            normalizer.variant.into(),
        );
        let parent = tempdir().unwrap();
        let root = parent.path().join("out");
        let error = Engine::new(composition)
            .unwrap()
            .extract_bytes(&bytes, &root)
            .unwrap_err();
        assert!(matches!(error, ZipError::UnsafePath(_)));
        assert!(!parent.path().join("escape.txt").exists());
    }
}

#[cfg(unix)]
#[test]
fn refuses_symlink_ancestor_escape() {
    use std::os::unix::fs::symlink;
    let bytes = archive(
        &[Item {
            name: "link/file.txt",
            bytes: b"no",
            deflate: false,
        }],
        b"",
    );
    let base = tempdir().unwrap();
    let root = base.path().join("root");
    let outside = base.path().join("outside");
    std::fs::create_dir_all(&root).unwrap();
    std::fs::create_dir_all(&outside).unwrap();
    symlink(&outside, root.join("link")).unwrap();
    assert!(matches!(
        Engine::new(Composition::default())
            .unwrap()
            .extract_bytes(&bytes, &root),
        Err(ZipError::UnsafePath(_))
    ));
    assert!(!outside.join("file.txt").exists());
}

#[test]
fn crc_corruption_is_observable() {
    let mut bytes = representative();
    let central = bytes
        .windows(4)
        .position(|w| w == 0x0201_4b50u32.to_le_bytes())
        .unwrap();
    bytes[central + 16] ^= 0x80;
    let dir = tempdir().unwrap();
    assert!(matches!(
        Engine::new(Composition::default())
            .unwrap()
            .extract_bytes(&bytes, dir.path()),
        Err(ZipError::CrcMismatch { .. })
    ));
}

#[test]
fn supports_central_sizes_with_data_descriptor_flag() {
    let mut bytes = archive(
        &[Item {
            name: "descriptor.txt",
            bytes: b"descriptor",
            deflate: true,
        }],
        b"",
    );
    let central = bytes
        .windows(4)
        .position(|w| w == 0x0201_4b50u32.to_le_bytes())
        .unwrap();
    bytes[6..8].copy_from_slice(&(0x0808u16).to_le_bytes());
    bytes[14..26].fill(0);
    bytes[central + 8..central + 10].copy_from_slice(&(0x0808u16).to_le_bytes());
    let crc = u32::from_le_bytes(bytes[central + 16..central + 20].try_into().unwrap());
    let compressed = u32::from_le_bytes(bytes[central + 20..central + 24].try_into().unwrap());
    let uncompressed = u32::from_le_bytes(bytes[central + 24..central + 28].try_into().unwrap());
    let mut descriptor = Vec::new();
    push32(&mut descriptor, 0x0807_4b50);
    push32(&mut descriptor, crc);
    push32(&mut descriptor, compressed);
    push32(&mut descriptor, uncompressed);
    bytes.splice(central..central, descriptor);
    let new_central = central + 16;
    let eocd = bytes
        .windows(4)
        .position(|w| w == 0x0605_4b50u32.to_le_bytes())
        .unwrap();
    bytes[eocd + 16..eocd + 20].copy_from_slice(&(new_central as u32).to_le_bytes());
    let dir = tempdir().unwrap();
    Engine::new(Composition::default())
        .unwrap()
        .extract_bytes(&bytes, dir.path())
        .unwrap();
    assert_eq!(
        std::fs::read(dir.path().join("descriptor.txt")).unwrap(),
        b"descriptor"
    );
}

#[test]
fn rejects_local_and_central_filename_mismatch() {
    let mut bytes = archive(
        &[Item {
            name: "same.txt",
            bytes: b"ok",
            deflate: false,
        }],
        b"",
    );
    bytes[30] = b'l';
    let dir = tempdir().unwrap();
    assert!(matches!(
        Engine::new(Composition::default())
            .unwrap()
            .extract_bytes(&bytes, dir.path()),
        Err(ZipError::Invalid("local/central filename mismatch"))
    ));
}

#[test]
fn malformed_and_unsupported_archives_fail_closed() {
    assert!(Engine::new(Composition::default())
        .unwrap()
        .list_bytes(b"not a zip")
        .is_err());
    let mut truncated = representative();
    truncated.truncate(17);
    assert!(Engine::new(Composition::default())
        .unwrap()
        .list_bytes(&truncated)
        .is_err());
    let mut encrypted = representative();
    encrypted[6] |= 1;
    assert!(Engine::new(Composition::default())
        .unwrap()
        .extract_bytes(&encrypted, Path::new("unused"))
        .is_err());
}

#[test]
fn decodes_legacy_cp437_and_rejects_invalid_declared_utf8() {
    let mut bytes = archive(
        &[Item {
            name: "x.txt",
            bytes: b"ok",
            deflate: false,
        }],
        b"",
    );
    let central = bytes
        .windows(4)
        .position(|w| w == 0x0201_4b50u32.to_le_bytes())
        .unwrap();
    bytes[6..8].copy_from_slice(&0u16.to_le_bytes());
    bytes[30] = 0x82;
    bytes[central + 8..central + 10].copy_from_slice(&0u16.to_le_bytes());
    bytes[central + 46] = 0x82;
    let decoders: Vec<_> = variants_catalog()
        .into_iter()
        .filter(|variant| variant.function == "decode_filename" && !variant.experimental)
        .collect();
    for decoder in &decoders {
        let mut composition = Composition::default();
        composition
            .implementations
            .insert("decode_filename".into(), decoder.variant.into());
        let entries = Engine::new(composition)
            .unwrap()
            .list_bytes(&bytes)
            .unwrap_or_else(|error| panic!("{} failed CP437: {error}", decoder.variant));
        assert_eq!(entries[0].name, "é.txt");
    }

    bytes[6..8].copy_from_slice(&(1u16 << 11).to_le_bytes());
    bytes[central + 8..central + 10].copy_from_slice(&(1u16 << 11).to_le_bytes());
    for decoder in decoders {
        let mut composition = Composition::default();
        composition
            .implementations
            .insert("decode_filename".into(), decoder.variant.into());
        assert!(
            matches!(
                Engine::new(composition).unwrap().list_bytes(&bytes),
                Err(ZipError::InvalidUtf8)
            ),
            "{} accepted invalid declared UTF-8",
            decoder.variant
        );
    }
}

#[test]
fn controlled_faults_have_narrow_reproducible_triggers() {
    let long_comment = vec![b'x'; 2_000];
    let bytes = archive(
        &[Item {
            name: "ok.txt",
            bytes: b"ok",
            deflate: false,
        }],
        &long_comment,
    );
    let mut bad = Composition::default();
    bad.implementations.insert(
        "locate_end_of_central_directory".into(),
        "faulty_short_comment".into(),
    );
    assert!(Engine::new(bad).unwrap().list_bytes(&bytes).is_err());
    for locator in variants_catalog().into_iter().filter(|variant| {
        variant.function == "locate_end_of_central_directory" && !variant.experimental
    }) {
        let mut composition = Composition::default();
        composition.implementations.insert(
            "locate_end_of_central_directory".into(),
            locator.variant.into(),
        );
        assert!(
            Engine::new(composition).unwrap().list_bytes(&bytes).is_ok(),
            "{} rejected a valid long comment",
            locator.variant
        );
    }

    let nested = archive(
        &[Item {
            name: "a/b/c.txt",
            bytes: b"ok",
            deflate: false,
        }],
        b"",
    );
    let mut bad = Composition::default();
    bad.implementations.insert(
        "normalize_extraction_path".into(),
        "faulty_depth_limit".into(),
    );
    assert!(Engine::new(bad)
        .unwrap()
        .extract_bytes(&nested, tempdir().unwrap().path())
        .is_err());
}

#[test]
fn composition_accepts_schema_and_rejects_unknown_variants() {
    let json = r#"{"client_id":"pc_12345678","release_id":"r1","revision":2,"implementations":{"list_archive":"list_archive.preallocated"},"reason":"registration"}"#;
    let composition = Composition::from_json(json).unwrap();
    assert_eq!(
        composition.variant("list_archive"),
        "list_archive.preallocated"
    );
    let mut invalid = HashMap::new();
    invalid.insert("list_archive".into(), "made_up".into());
    assert!(Engine::new(Composition {
        implementations: invalid,
        composition_id: None
    })
    .is_err());
}

#[test]
fn variants_json_is_backend_release_payload() {
    let output = std::process::Command::new(env!("CARGO_BIN_EXE_zipdiv"))
        .args(["variants", "--json"])
        .output()
        .unwrap();
    assert!(output.status.success());
    let value: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
    assert_eq!(value["application"], "zipdiv");
    assert_eq!(
        value["spec_functions"].as_array().unwrap().len(),
        SPEC_FUNCTIONS.len()
    );
    for function in value["spec_functions"].as_array().unwrap() {
        assert!(function["implementations"].as_array().unwrap().len() >= 5);
    }
}
