use pyaxml_rs::arsc::locale_tag;
use pyaxml_rs::axml::Axml;
use std::path::Path;

/// Read test data file relative to the workspace root.
fn read_test_file(relative: &str) -> Vec<u8> {
    let base = Path::new(env!("CARGO_MANIFEST_DIR"));
    let path = base.join(relative);
    std::fs::read(&path).unwrap_or_else(|e| panic!("Failed to read {}: {}", path.display(), e))
}

/// Assert that parsing then packing produces identical bytes.
macro_rules! assert_round_trip {
    ($path:expr) => {{
        let data = read_test_file($path);
        let axml =
            Axml::from_axml(&data).unwrap_or_else(|e| panic!("Failed to parse {}: {}", $path, e));
        let repacked = axml.pack();
        assert_eq!(
            data,
            repacked,
            "Round-trip mismatch for {}: original {} bytes, repacked {} bytes",
            $path,
            data.len(),
            repacked.len()
        );
    }};
}

/// Assert that parse → to_xml → reparse → to_xml produces the same XML string.
macro_rules! assert_xml_stable {
    ($path:expr) => {{
        let data = read_test_file($path);
        let axml1 =
            Axml::from_axml(&data).unwrap_or_else(|e| panic!("Failed to parse {}: {}", $path, e));
        let xml1 = axml1
            .to_xml()
            .unwrap_or_else(|e| panic!("Failed to_xml (first) for {}: {}", $path, e));
        let repacked = axml1.pack();
        let axml2 = Axml::from_axml(&repacked)
            .unwrap_or_else(|e| panic!("Failed to reparse {}: {}", $path, e));
        let xml2 = axml2
            .to_xml()
            .unwrap_or_else(|e| panic!("Failed to_xml (second) for {}", $path));
        assert_eq!(xml1, xml2, "XML stability mismatch for {}", $path);
    }};
}

/// Assert the full decode → encode → decode cycle:
///   1. binary  → xml1         (decode)
///   2. xml1    → binary2      (encode via from_xml + pack)
///   3. binary2 → xml2         (decode again)
///   4. xml1 == xml2            (decode output is stable)
///   5. xml2    → binary3      (second encode)
///   6. binary2 == binary3     (encode output is idempotent)
macro_rules! assert_decode_encode_decode {
    ($path:expr) => {{
        let data = read_test_file($path);

        // Step 1: decode
        let axml1 =
            Axml::from_axml(&data).unwrap_or_else(|e| panic!("decode failed for {}: {}", $path, e));
        let xml1 = axml1
            .to_xml()
            .unwrap_or_else(|e| panic!("to_xml (step 1) failed for {}: {}", $path, e));

        // Step 2: encode
        let mut axml2 = Axml::new();
        axml2
            .from_xml(&xml1)
            .unwrap_or_else(|e| panic!("from_xml (step 2) failed for {}: {}", $path, e));
        let binary2 = axml2.pack();

        // Step 3: decode again
        let axml3 = Axml::from_axml(&binary2).unwrap_or_else(|e| {
            panic!(
                "re-parse (step 3) failed for {}: {}, binary is {} bytes",
                $path,
                e,
                binary2.len()
            )
        });
        let xml2 = axml3
            .to_xml()
            .unwrap_or_else(|e| panic!("to_xml (step 3) failed for {}: {}", $path, e));

        // Step 4: XML must be the same after encode → decode
        assert_eq!(
            xml1, xml2,
            "decode→encode→decode XML mismatch for {}",
            $path
        );

        // Steps 5–6: second encode must produce identical bytes
        let mut axml4 = Axml::new();
        axml4
            .from_xml(&xml2)
            .unwrap_or_else(|e| panic!("from_xml (step 5) failed for {}: {}", $path, e));
        let binary3 = axml4.pack();
        assert_eq!(
            binary2,
            binary3,
            "encode is not idempotent for {} ({} vs {} bytes)",
            $path,
            binary2.len(),
            binary3.len()
        );
    }};
}

// ─────────────────────── Round-trip byte equality tests ──────────────────────

const ROUND_TRIP_FILES: &[&str] = &[
    "tests/data/AXML/AndroidManifest.xml",
    "tests/data/AXML/AndroidManifest-Chinese.xml",
    "tests/data/AXML/AndroidManifestDoubleNamespace.xml",
    "tests/data/AXML/AndroidManifestExtraNamespace.xml",
    "tests/data/AXML/AndroidManifest_InvalidCharsInAttribute.xml",
    "tests/data/AXML/AndroidManifestLiapp.xml",
    "tests/data/AXML/AndroidManifestMaskingNamespace.xml",
    "tests/data/AXML/AndroidManifest_NamespaceInAttributeName.xml",
    "tests/data/AXML/AndroidManifest_NamespaceInAttributeName2.xml",
    "tests/data/AXML/AndroidManifestNonZeroStyle.xml",
    "tests/data/AXML/AndroidManifestNullbytes.xml",
    "tests/data/AXML/AndroidManifestTextChunksXML.xml",
    "tests/data/AXML/AndroidManifestUTF8Strings.xml",
    "tests/data/AXML/AndroidManifestWithComment.xml",
    "tests/data/AXML/AndroidManifest_WrongChunkStart.xml",
    "tests/data/AXML/AndroidManifest-xmlns.xml",
    "tests/data/AXML/StringBlocksNotAligned.xml",
];

#[test]
fn test_round_trip_all() {
    for path in ROUND_TRIP_FILES {
        assert_round_trip!(path);
    }
}

// ─────────────── XML stability (to_xml idempotency after repack) ─────────────

const XML_STABLE_FILES: &[&str] = &[
    "tests/data/AXML/AndroidManifest.xml",
    "tests/data/AXML/AndroidManifest-Chinese.xml",
    "tests/data/AXML/AndroidManifestDoubleNamespace.xml",
    "tests/data/AXML/AndroidManifestExtraNamespace.xml",
    "tests/data/AXML/AndroidManifestLiapp.xml",
    "tests/data/AXML/AndroidManifestMaskingNamespace.xml",
    "tests/data/AXML/AndroidManifest_NamespaceInAttributeName.xml",
    "tests/data/AXML/AndroidManifest_NamespaceInAttributeName2.xml",
    "tests/data/AXML/AndroidManifestNonZeroStyle.xml",
    "tests/data/AXML/AndroidManifestWithComment.xml",
    "tests/data/AXML/AndroidManifest_WrongChunkStart.xml",
    "tests/data/AXML/AndroidManifest-xmlns.xml",
    "tests/data/AXML/StringBlocksNotAligned.xml",
];

#[test]
fn test_xml_stable_all() {
    for path in XML_STABLE_FILES {
        assert_xml_stable!(path);
    }
}

// ──── decode → encode → decode idempotency (full CLI round-trip equivalent) ───

const DECODE_ENCODE_DECODE_FILES: &[&str] = &[
    "tests/data/AXML/AndroidManifest.xml",
    "tests/data/AXML/AndroidManifest-Chinese.xml",
    "tests/data/AXML/AndroidManifestDoubleNamespace.xml",
    "tests/data/AXML/AndroidManifestExtraNamespace.xml",
    "tests/data/AXML/AndroidManifestLiapp.xml",
    "tests/data/AXML/AndroidManifestMaskingNamespace.xml",
    "tests/data/AXML/AndroidManifest_NamespaceInAttributeName.xml",
    "tests/data/AXML/AndroidManifest_NamespaceInAttributeName2.xml",
    "tests/data/AXML/AndroidManifestNonZeroStyle.xml",
    "tests/data/AXML/AndroidManifestWithComment.xml",
    "tests/data/AXML/AndroidManifest_WrongChunkStart.xml",
    "tests/data/AXML/AndroidManifest-xmlns.xml",
    "tests/data/AXML/StringBlocksNotAligned.xml",
];

#[test]
fn test_decode_encode_decode_all() {
    for path in DECODE_ENCODE_DECODE_FILES {
        assert_decode_encode_decode!(path);
    }
}

// ─────────────────────────── Parse smoke tests ────────────────────────────────

#[test]
fn test_parse_all_manifests() {
    let files = &[
        "tests/data/AXML/AndroidManifest.xml",
        "tests/data/AXML/AndroidManifest-Chinese.xml",
        "tests/data/AXML/AndroidManifestDoubleNamespace.xml",
        "tests/data/AXML/AndroidManifestExtraNamespace.xml",
        "tests/data/AXML/AndroidManifest_InvalidCharsInAttribute.xml",
        "tests/data/AXML/AndroidManifestLiapp.xml",
        "tests/data/AXML/AndroidManifestMaskingNamespace.xml",
        "tests/data/AXML/AndroidManifest_NamespaceInAttributeName.xml",
        "tests/data/AXML/AndroidManifest_NamespaceInAttributeName2.xml",
        "tests/data/AXML/AndroidManifestNonZeroStyle.xml",
        "tests/data/AXML/AndroidManifestNullbytes.xml",
        "tests/data/AXML/AndroidManifestTextChunksXML.xml",
        "tests/data/AXML/AndroidManifestWithComment.xml",
        "tests/data/AXML/AndroidManifest_WrongChunkStart.xml",
        "tests/data/AXML/AndroidManifest-xmlns.xml",
    ];
    for path in files {
        let data = read_test_file(path);
        let axml =
            Axml::from_axml(&data).unwrap_or_else(|e| panic!("Failed to parse {}: {}", path, e));
        assert!(axml.string_count() > 0, "Empty string pool for {}", path);
    }
}

#[test]
fn test_non_manifest_root_tag() {
    let files = &[
        "tests/data/AXML/test.xml",
        "tests/data/AXML/test1.xml",
        "tests/data/AXML/test2.xml",
        "tests/data/AXML/test3.xml",
    ];
    for path in files {
        let data = read_test_file(path);
        let axml =
            Axml::from_axml(&data).unwrap_or_else(|e| panic!("Failed to parse {}: {}", path, e));
        let xml = axml
            .to_xml()
            .unwrap_or_else(|e| panic!("to_xml failed for {}: {}", path, e));
        assert!(
            xml.contains("<LinearLayout"),
            "Expected LinearLayout root in {}, got: {}",
            path,
            &xml[..xml.len().min(200)]
        );
    }
}

#[test]
fn test_utf8_strings() {
    let data = read_test_file("tests/data/AXML/AndroidManifestUTF8Strings.xml");
    let axml = Axml::from_axml(&data).expect("parse UTF8 manifest");
    let repacked = axml.pack();
    assert_eq!(data.len(), repacked.len(), "UTF8 round-trip size mismatch");
    assert_eq!(data, repacked, "UTF8 round-trip content mismatch");
}

#[test]
fn test_nonzero_style_offset() {
    let data = read_test_file("tests/data/AXML/AndroidManifestNonZeroStyle.xml");
    let axml = Axml::from_axml(&data).expect("parse NonZeroStyle manifest");
    // Should not panic
    let _ = axml.to_xml().expect("to_xml should succeed");
}

#[test]
fn test_null_bytes() {
    let data = read_test_file("tests/data/AXML/AndroidManifestNullbytes.xml");
    let axml = Axml::from_axml(&data).expect("parse Nullbytes manifest");
    let _ = axml.to_xml(); // Should not panic; may produce lossy output
}

#[test]
fn test_string_blocks_not_aligned() {
    let data = read_test_file("tests/data/AXML/StringBlocksNotAligned.xml");
    let axml = Axml::from_axml(&data).expect("parse StringBlocksNotAligned");
    let repacked = axml.pack();
    assert_eq!(data, repacked, "StringBlocksNotAligned round-trip failed");
}

#[test]
fn test_from_xml_round_trip() {
    // Build AXML from a simple XML string and verify it round-trips
    let xml = r#"<?xml version='1.0' encoding='utf-8'?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.example.test"
  android:versionCode="1"
  android:versionName="1.0">
  <application android:label="Test App">
    <activity android:name=".MainActivity" android:exported="true" />
  </application>
</manifest>"#;

    let mut axml = Axml::new();
    axml.from_xml(xml).expect("from_xml should succeed");
    let binary = axml.pack();

    // Parse back and get XML
    let axml2 = Axml::from_axml(&binary).expect("re-parse should succeed");
    let xml2 = axml2.to_xml().expect("to_xml should succeed");

    // Parse the repacked binary again
    let binary2 = axml2.pack();
    let axml3 = Axml::from_axml(&binary2).expect("third parse should succeed");
    let xml3 = axml3.to_xml().expect("third to_xml should succeed");

    assert_eq!(xml2, xml3, "from_xml → pack → to_xml should be idempotent");

    // Basic sanity check on content
    assert!(xml2.contains("manifest"), "Should contain 'manifest'");
    assert!(
        xml2.contains("com.example.test"),
        "Should contain package name"
    );
}

#[test]
fn test_invalid_magic_error() {
    let bad_data = vec![0xFF, 0xFF, 0x08, 0x00, 0x10, 0x00, 0x00, 0x00];
    let result = Axml::from_axml(&bad_data);
    assert!(result.is_err(), "Should fail on invalid magic");
}

#[test]
fn test_too_short_error() {
    let result = Axml::from_axml(&[0x03, 0x00]);
    assert!(result.is_err(), "Should fail on truncated input");
}

#[test]
fn test_header_size_exceeds_data() {
    // file_header_size (bytes 2-3) = 28024, file_size (bytes 4-7) = 5632,
    // but actual data is only 8 bytes, must return an error, not panic.
    let mut data = vec![0x03, 0x00]; // type = XML
    data.extend_from_slice(&28024u16.to_le_bytes()); // header_size = 28024
    data.extend_from_slice(&5632u32.to_le_bytes()); // file_size = 5632
    let result = Axml::from_axml(&data);
    assert!(
        result.is_err(),
        "Should fail when header_size exceeds data length"
    );
}

// ──────────────── XML entity decoding in attribute values ────────────────────

#[test]
fn test_xml_entities_in_attribute_values() {
    // Regression: attribute values containing XML entities (e.g. &gt; &lt; &amp;)
    // must be stored as their decoded characters in the binary, not as literal
    // entity sequences.  Without the fix, re-decoding produced &amp;gt; instead
    // of &gt; because the raw 4-char sequence `&gt;` was stored in the string pool
    // and then xml_escape_into() re-encoded the `&` to `&amp;`.
    let xml = r#"<?xml version='1.0' encoding='utf-8'?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.example.test">
  <application android:label="A &gt; B"
               android:description="x &lt; y &amp; z &quot;q&quot;" />
</manifest>"#;

    let mut axml = Axml::new();
    axml.from_xml(xml).expect("from_xml with entities");
    let binary = axml.pack();

    let axml2 = Axml::from_axml(&binary).expect("re-parse after entity encode");
    let xml2 = axml2.to_xml().expect("to_xml after entity re-parse");

    // The decoded XML must use the properly escaped forms, not double-encoded ones.
    assert!(
        xml2.contains("android:label=\"A &gt; B\""),
        "label should contain &gt; not &amp;gt;, got:\n{}",
        xml2
    );
    assert!(
        xml2.contains("&lt; y &amp; z"),
        "description entities should round-trip correctly, got:\n{}",
        xml2
    );
    assert!(
        !xml2.contains("&amp;gt;"),
        "should not contain double-encoded &amp;gt;, got:\n{}",
        xml2
    );
    assert!(
        !xml2.contains("&amp;lt;"),
        "should not contain double-encoded &amp;lt;, got:\n{}",
        xml2
    );
    assert!(
        !xml2.contains("&amp;amp;"),
        "should not contain double-encoded &amp;amp;, got:\n{}",
        xml2
    );
}

// ─────────────────────────── Proto round-trip tests ──────────────────────────

/// Parse binary AXML → convert to proto bytes → reconstruct → XML must match.
#[test]
fn test_proto_round_trip_xml() {
    for path in ROUND_TRIP_FILES {
        let data = read_test_file(path);
        let axml =
            Axml::from_axml(&data).unwrap_or_else(|e| panic!("parse failed for {}: {}", path, e));
        let xml_before = axml
            .to_xml()
            .unwrap_or_else(|e| panic!("to_xml before proto for {}: {}", path, e));

        let proto_bytes = axml.to_proto_bytes();
        let axml2 = Axml::from_proto_bytes(&proto_bytes)
            .unwrap_or_else(|e| panic!("from_proto_bytes failed for {}: {}", path, e));
        let xml_after = axml2
            .to_xml()
            .unwrap_or_else(|e| panic!("to_xml after proto for {}: {}", path, e));

        assert_eq!(
            xml_before, xml_after,
            "Proto round-trip changed XML for {}",
            path
        );
    }
}

// ──────────────── Full XML→binary→XML round-trip via test.xml ────────────────

#[test]
fn test_from_xml_full_roundtrip_real() {
    // This test reproduces: axml2xml -i APK → xml2axml → axml2xml → "Unexpected end of file"
    // Uses the test.xml generated from the Runkeeper APK (if available).
    let test_xml_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("test.xml");
    if !test_xml_path.exists() {
        eprintln!("SKIP: test.xml not found at {}", test_xml_path.display());
        return;
    }
    let xml = std::fs::read_to_string(&test_xml_path).expect("failed to read test.xml");

    let mut axml = Axml::new();
    axml.from_xml(&xml)
        .expect("from_xml should parse the Runkeeper XML");
    let binary = axml.pack();

    let axml2 = Axml::from_axml(&binary)
        .unwrap_or_else(|e| panic!("re-parse of from_xml output failed: {}", e));
    let xml2 = axml2
        .to_xml()
        .expect("to_xml on re-parsed AXML should succeed");
    assert!(
        xml2.contains("manifest"),
        "re-parsed XML should contain 'manifest'"
    );
}

#[test]
fn test_from_xml_file_write_read_roundtrip() {
    // Regression test for: read_binary_data() prepended magic bytes AND re-read the
    // full file after seek(0), duplicating the first 2 bytes and corrupting the header.
    // Simulate the CLI path: write pack() output to disk and read back with std::fs::read.
    let xml = r#"<?xml version='1.0' encoding='utf-8'?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.example.regress"
  android:versionCode="42"
  android:versionName="1.0">
  <application android:label="Test" />
</manifest>"#;

    let mut axml = Axml::new();
    axml.from_xml(xml).expect("from_xml");
    let binary = axml.pack();

    // Write to a temp file and read back (simulates CLI file I/O)
    let tmp = std::env::temp_dir().join("pyaxml_regress_test.axml");
    std::fs::write(&tmp, &binary).expect("write temp file");
    let read_back = std::fs::read(&tmp).expect("read temp file");
    std::fs::remove_file(&tmp).ok();

    assert_eq!(binary, read_back, "file I/O must preserve bytes exactly");

    // Now parse the re-read bytes, must not fail with Unexpected end of file
    let axml2 = Axml::from_axml(&read_back).expect("from_axml on file-written binary must succeed");
    let xml2 = axml2.to_xml().expect("to_xml must succeed");
    assert!(xml2.contains("manifest"));
}

// ─────────────────────────── locale_tag unit tests ───────────────────────────

#[test]
fn test_locale_tag_default() {
    assert_eq!(locale_tag("", ""), "default");
}

#[test]
fn test_locale_tag_language_only() {
    assert_eq!(locale_tag("en", ""), "en");
}

#[test]
fn test_locale_tag_language_and_region() {
    assert_eq!(locale_tag("en", "US"), "en-US");
}

// ─────────────────────────── ARSC list_packages tests ────────────────────────

#[test]
fn test_arsc_list_packages_has_resource_tags() {
    use pyaxml_rs::arsc::Arsc;
    let data = read_test_file("tests/data/ARSC/example.arsc");
    let arsc = Arsc::from_axml(&data).expect("parse ARSC");
    let xml = arsc.list_packages(None);
    assert!(
        xml.contains("<resources lang="),
        "list_packages output should contain <resources lang=> sections"
    );
}

#[test]
fn test_arsc_list_packages_filter_default() {
    use pyaxml_rs::arsc::Arsc;
    let data = read_test_file("tests/data/ARSC/example.arsc");
    let arsc = Arsc::from_axml(&data).expect("parse ARSC");
    let xml = arsc.list_packages(Some("default"));
    // Should only contain lang="default" sections
    assert!(
        xml.contains("lang=\"default\""),
        "Filtered output should contain lang=\"default\""
    );
    // Count distinct lang= values, should only be "default"
    for line in xml.lines() {
        if line.starts_with("<resources lang=") {
            assert!(
                line.contains("lang=\"default\""),
                "Filtered output should only have default locale, got: {}",
                line
            );
        }
    }
}

#[test]
fn test_arsc_proto_round_trip() {
    use pyaxml_rs::arsc::Arsc;

    let data = read_test_file("tests/data/ARSC/example.arsc");
    let arsc1 = Arsc::from_axml(&data).expect("parse ARSC");

    // Serialize to proto bytes and deserialize, both should complete without panic
    let proto_bytes = arsc1.to_proto_bytes();
    assert!(
        !proto_bytes.is_empty(),
        "to_proto_bytes should produce non-empty output"
    );

    let arsc2 = Arsc::from_proto_bytes(&proto_bytes).expect("from_proto_bytes");
    let list2 = arsc2.list_packages(None);
    assert!(
        !list2.is_empty(),
        "ARSC deserialized from proto should produce non-empty resource listing"
    );
}

#[test]
fn test_arsc_compute_preserves_binary() {
    use pyaxml_rs::arsc::Arsc;

    let data = read_test_file("tests/data/ARSC/example.arsc");
    let mut arsc = Arsc::from_axml(&data).expect("parse ARSC");

    arsc.compute(true);
    let packed = arsc.pack();

    // parse → compute(recursive=true) → pack must reproduce the original binary
    // exactly when no modification was made.  If this fails it means compute()
    // is re-encoding the string pools unnecessarily.
    assert_eq!(
        data.len(),
        packed.len(),
        "packed size {} differs from original {}",
        packed.len(),
        data.len()
    );
    assert_eq!(
        data, packed,
        "parse → compute(recursive=true) → pack must be binary-identical to the input \
         when no modification was made"
    );
}

#[test]
fn test_arsc_round_trip_entry_offsets() {
    use pyaxml_rs::arsc::Arsc;

    let data = read_test_file("tests/data/ARSC/example.arsc");
    let mut arsc1 = Arsc::from_axml(&data).expect("parse ARSC");

    // Compute sizes and pack, this is what apkpatcher does
    arsc1.compute(true);
    let packed = arsc1.pack();

    // The critical test: can Android's parser successfully read the repacked ARSC?
    // If entry offsets were wrong, this would panic or return invalid resources.
    let arsc2 =
        Arsc::from_axml(&packed).expect("parse packed ARSC (entry offsets must be correct)");

    // Verify both produced valid resource listings
    let list1 = arsc1.list_packages(None);
    let list2 = arsc2.list_packages(None);
    assert!(!list1.is_empty(), "Original ARSC should have resources");
    assert!(
        !list2.is_empty(),
        "Packed ARSC should have resources after entry offset fix"
    );
}

#[test]
fn test_arsc_compute_pack_idempotency() {
    use pyaxml_rs::arsc::Arsc;

    let data = read_test_file("tests/data/ARSC/example.arsc");
    let mut arsc1 = Arsc::from_axml(&data).expect("parse ARSC");

    // First pass: compute and pack
    arsc1.compute(true);
    let packed1 = arsc1.pack();

    // Parse the packed version
    let mut arsc2 = Arsc::from_axml(&packed1).expect("parse first pack result");

    // Second pass: compute and pack the repacked ARSC
    arsc2.compute(true);
    let packed2 = arsc2.pack();

    // Binary idempotency: second pack should produce identical bytes
    // This verifies that compute() correctly recalculates all sizes so that
    // a second pack cycle produces the same binary output (entry offsets now fixed).
    assert_eq!(
        packed1.len(),
        packed2.len(),
        "ARSC packed sizes must match after compute→pack→parse→compute→pack"
    );

    assert_eq!(
        packed1, packed2,
        "ARSC binary must be identical after second compute→pack cycle (idempotency test)"
    );
}
