use super::*;
use crate::app::runtime_compare::{
    RuntimeCandidateTableEntrySample, RuntimeCandidateTableSample, RuntimeClassicProfileSample,
    RuntimeRecipeBookLineSample, RuntimeRt3105ProfileSample, RuntimeSetupLaunchPayloadSample,
    RuntimeSetupPayloadCoreSample, collect_numbered_candidate_name_runs,
    diff_candidate_table_samples, diff_classic_profile_samples,
    diff_recipe_book_line_content_samples, diff_recipe_book_line_samples,
    diff_rt3_105_profile_samples, diff_setup_launch_payload_samples,
    diff_setup_payload_core_samples,
};

#[test]
fn diffs_classic_profile_samples_across_multiple_files() {
    let sample_a = RuntimeClassicProfileSample {
        path: "a.gms".to_string(),
        profile_family: "rt3-classic-save-container-v1".to_string(),
        progress_32dc_offset: 0x76e8,
        progress_3714_offset: 0x76ec,
        progress_3715_offset: 0x77f8,
        packed_profile_offset: 0x76f0,
        packed_profile_len: 0x108,
        packed_profile_block: SmpClassicPackedProfileBlock {
            relative_len: 0x108,
            relative_len_hex: "0x108".to_string(),
            leading_word_0: 0x03000000,
            leading_word_0_hex: "0x03000000".to_string(),
            trailing_zero_word_count_after_leading_word: 3,
            map_path_offset: 0x13,
            map_path: Some("British Isles.gmp".to_string()),
            display_name_offset: 0x46,
            display_name: Some("British Isles".to_string()),
            profile_byte_0x77: 0,
            profile_byte_0x77_hex: "0x00".to_string(),
            profile_byte_0x82: 0,
            profile_byte_0x82_hex: "0x00".to_string(),
            profile_byte_0x97: 0,
            profile_byte_0x97_hex: "0x00".to_string(),
            profile_byte_0xc5: 0,
            profile_byte_0xc5_hex: "0x00".to_string(),
            stable_nonzero_words: vec![SmpPackedProfileWordLane {
                relative_offset: 0,
                relative_offset_hex: "0x00".to_string(),
                value: 0x03000000,
                value_hex: "0x03000000".to_string(),
            }],
        },
    };
    let mut sample_b = sample_a.clone();
    sample_b.path = "b.gms".to_string();
    sample_b.packed_profile_block.leading_word_0 = 0x05000000;
    sample_b.packed_profile_block.leading_word_0_hex = "0x05000000".to_string();
    sample_b.packed_profile_block.stable_nonzero_words[0].value = 0x05000000;
    sample_b.packed_profile_block.stable_nonzero_words[0].value_hex = "0x05000000".to_string();

    let differences =
        diff_classic_profile_samples(&[sample_a, sample_b]).expect("diff should succeed");

    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.packed_profile_block.leading_word_0")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.packed_profile_block.leading_word_0_hex")
    );
    assert!(differences.iter().any(
            |entry| entry.field_path == "$.packed_profile_block.stable_nonzero_words[0].value"
        ));
}

#[test]
fn diffs_rt3_105_profile_samples_across_multiple_files() {
    let sample_a = RuntimeRt3105ProfileSample {
        path: "a.gms".to_string(),
        profile_family: "rt3-105-save-container-v1".to_string(),
        packed_profile_offset: 0x73c0,
        packed_profile_len: 0x108,
        packed_profile_block: SmpRt3105PackedProfileBlock {
            relative_len: 0x108,
            relative_len_hex: "0x108".to_string(),
            leading_word_0: 3,
            leading_word_0_hex: "0x00000003".to_string(),
            trailing_zero_word_count_after_leading_word: 2,
            header_flag_word_3: 0x01000000,
            header_flag_word_3_hex: "0x01000000".to_string(),
            map_path_offset: 0x10,
            map_path: Some("Alternate USA.gmp".to_string()),
            display_name_offset: 0x43,
            display_name: Some("Alternate USA".to_string()),
            profile_byte_0x77: 0x07,
            profile_byte_0x77_hex: "0x07".to_string(),
            profile_byte_0x82: 0x4d,
            profile_byte_0x82_hex: "0x4d".to_string(),
            profile_byte_0x97: 0x00,
            profile_byte_0x97_hex: "0x00".to_string(),
            profile_byte_0xc5: 0x00,
            profile_byte_0xc5_hex: "0x00".to_string(),
            stable_nonzero_words: vec![SmpPackedProfileWordLane {
                relative_offset: 0x80,
                relative_offset_hex: "0x80".to_string(),
                value: 0x364d0000,
                value_hex: "0x364d0000".to_string(),
            }],
        },
    };
    let mut sample_b = sample_a.clone();
    sample_b.path = "b.gms".to_string();
    sample_b.profile_family = "rt3-105-alt-save-container-v1".to_string();
    sample_b.packed_profile_block.map_path = Some("Southern Pacific.gmp".to_string());
    sample_b.packed_profile_block.display_name = Some("Southern Pacific".to_string());
    sample_b.packed_profile_block.leading_word_0 = 5;
    sample_b.packed_profile_block.leading_word_0_hex = "0x00000005".to_string();
    sample_b.packed_profile_block.profile_byte_0x82 = 0x90;
    sample_b.packed_profile_block.profile_byte_0x82_hex = "0x90".to_string();
    sample_b.packed_profile_block.stable_nonzero_words[0].value = 0x1b900000;
    sample_b.packed_profile_block.stable_nonzero_words[0].value_hex = "0x1b900000".to_string();

    let differences =
        diff_rt3_105_profile_samples(&[sample_a, sample_b]).expect("diff should succeed");

    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.profile_family")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.packed_profile_block.map_path")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.packed_profile_block.profile_byte_0x82")
    );
}

#[test]
fn diffs_candidate_table_samples_across_multiple_files() {
    let mut availability_a = BTreeMap::new();
    availability_a.insert("AutoPlant".to_string(), 1u32);
    availability_a.insert("Nuclear Power Plant".to_string(), 0u32);

    let sample_a = RuntimeCandidateTableSample {
        path: "a.gmp".to_string(),
        profile_family: "rt3-105-map-container-v1".to_string(),
        source_kind: "map-fixed-catalog-range".to_string(),
        semantic_family: "scenario-named-candidate-availability-table".to_string(),
        header_word_0_hex: "0x10000000".to_string(),
        header_word_1_hex: "0x00009000".to_string(),
        header_word_2_hex: "0x0000332e".to_string(),
        observed_entry_count: 67,
        zero_trailer_entry_count: 1,
        nonzero_trailer_entry_count: 66,
        zero_trailer_entry_names: vec!["Nuclear Power Plant".to_string()],
        footer_progress_word_0_hex: "0x000032dc".to_string(),
        footer_progress_word_1_hex: "0x00003714".to_string(),
        availability_by_name: availability_a,
    };

    let mut availability_b = BTreeMap::new();
    availability_b.insert("AutoPlant".to_string(), 0u32);
    availability_b.insert("Nuclear Power Plant".to_string(), 0u32);

    let sample_b = RuntimeCandidateTableSample {
        path: "b.gmp".to_string(),
        profile_family: "rt3-105-scenario-map-container-v1".to_string(),
        source_kind: "map-fixed-catalog-range".to_string(),
        semantic_family: "scenario-named-candidate-availability-table".to_string(),
        header_word_0_hex: "0x00000000".to_string(),
        header_word_1_hex: "0x00000000".to_string(),
        header_word_2_hex: "0x0000332e".to_string(),
        observed_entry_count: 67,
        zero_trailer_entry_count: 2,
        nonzero_trailer_entry_count: 65,
        zero_trailer_entry_names: vec!["AutoPlant".to_string(), "Nuclear Power Plant".to_string()],
        footer_progress_word_0_hex: "0x000032dc".to_string(),
        footer_progress_word_1_hex: "0x00003714".to_string(),
        availability_by_name: availability_b,
    };

    let differences =
        diff_candidate_table_samples(&[sample_a, sample_b]).expect("diff should succeed");

    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.profile_family")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.header_word_0_hex")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.availability_by_name.AutoPlant")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.zero_trailer_entry_names[0]")
    );
}

#[test]
fn collects_numbered_candidate_name_runs_by_prefix() {
    let entries = vec![
        RuntimeCandidateTableEntrySample {
            index: 35,
            offset: 28535,
            text: "Port00".to_string(),
            availability_dword: 1,
            availability_dword_hex: "0x00000001".to_string(),
            trailer_word: 1,
            trailer_word_hex: "0x00000001".to_string(),
        },
        RuntimeCandidateTableEntrySample {
            index: 43,
            offset: 28807,
            text: "Warehouse00".to_string(),
            availability_dword: 1,
            availability_dword_hex: "0x00000001".to_string(),
            trailer_word: 1,
            trailer_word_hex: "0x00000001".to_string(),
        },
        RuntimeCandidateTableEntrySample {
            index: 45,
            offset: 28875,
            text: "Port01".to_string(),
            availability_dword: 1,
            availability_dword_hex: "0x00000001".to_string(),
            trailer_word: 1,
            trailer_word_hex: "0x00000001".to_string(),
        },
        RuntimeCandidateTableEntrySample {
            index: 46,
            offset: 28909,
            text: "Port02".to_string(),
            availability_dword: 1,
            availability_dword_hex: "0x00000001".to_string(),
            trailer_word: 1,
            trailer_word_hex: "0x00000001".to_string(),
        },
        RuntimeCandidateTableEntrySample {
            index: 56,
            offset: 29249,
            text: "Warehouse01".to_string(),
            availability_dword: 1,
            availability_dword_hex: "0x00000001".to_string(),
            trailer_word: 1,
            trailer_word_hex: "0x00000001".to_string(),
        },
        RuntimeCandidateTableEntrySample {
            index: 57,
            offset: 29283,
            text: "Warehouse02".to_string(),
            availability_dword: 1,
            availability_dword_hex: "0x00000001".to_string(),
            trailer_word: 1,
            trailer_word_hex: "0x00000001".to_string(),
        },
    ];

    let port_runs = collect_numbered_candidate_name_runs(&entries, "Port");
    let warehouse_runs = collect_numbered_candidate_name_runs(&entries, "Warehouse");

    assert_eq!(port_runs.len(), 2);
    assert_eq!(port_runs[0].first_name, "Port00");
    assert_eq!(port_runs[0].count, 1);
    assert_eq!(port_runs[1].first_name, "Port01");
    assert_eq!(port_runs[1].last_name, "Port02");
    assert_eq!(port_runs[1].count, 2);

    assert_eq!(warehouse_runs.len(), 2);
    assert_eq!(warehouse_runs[0].first_name, "Warehouse00");
    assert_eq!(warehouse_runs[0].count, 1);
    assert_eq!(warehouse_runs[1].first_name, "Warehouse01");
    assert_eq!(warehouse_runs[1].last_name, "Warehouse02");
    assert_eq!(warehouse_runs[1].count, 2);
}

#[test]
fn diffs_recipe_book_line_samples_across_multiple_files() {
    let sample_a = RuntimeRecipeBookLineSample {
        path: "a.gmp".to_string(),
        profile_family: "rt3-105-map-container-v1".to_string(),
        source_kind: "recipe-book-summary".to_string(),
        book_count: 12,
        book_stride_hex: "0x4e1".to_string(),
        line_count: 5,
        line_stride_hex: "0x30".to_string(),
        book_head_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
        book_line_area_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
        max_annual_production_word_hex_by_book: BTreeMap::from([(
            "book00".to_string(),
            "0x41200000".to_string(),
        )]),
        line_kind_by_path: BTreeMap::from([("book00.line00".to_string(), "mixed".to_string())]),
        mode_word_hex_by_path: BTreeMap::from([(
            "book00.line00".to_string(),
            "0x00000003".to_string(),
        )]),
        annual_amount_word_hex_by_path: BTreeMap::from([(
            "book00.line00".to_string(),
            "0x41a00000".to_string(),
        )]),
        supplied_cargo_token_word_hex_by_path: BTreeMap::from([(
            "book00.line00".to_string(),
            "0x00000017".to_string(),
        )]),
        demanded_cargo_token_word_hex_by_path: BTreeMap::from([(
            "book00.line00".to_string(),
            "0x0000002a".to_string(),
        )]),
    };
    let sample_b = RuntimeRecipeBookLineSample {
        path: "b.gms".to_string(),
        profile_family: "rt3-105-alt-save-container-v1".to_string(),
        source_kind: "recipe-book-summary".to_string(),
        book_count: 12,
        book_stride_hex: "0x4e1".to_string(),
        line_count: 5,
        line_stride_hex: "0x30".to_string(),
        book_head_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
        book_line_area_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
        max_annual_production_word_hex_by_book: BTreeMap::from([(
            "book00".to_string(),
            "0x41200000".to_string(),
        )]),
        line_kind_by_path: BTreeMap::from([("book00.line00".to_string(), "zero".to_string())]),
        mode_word_hex_by_path: BTreeMap::from([(
            "book00.line00".to_string(),
            "0x00000000".to_string(),
        )]),
        annual_amount_word_hex_by_path: BTreeMap::from([(
            "book00.line00".to_string(),
            "0x00000000".to_string(),
        )]),
        supplied_cargo_token_word_hex_by_path: BTreeMap::from([(
            "book00.line00".to_string(),
            "0x00000000".to_string(),
        )]),
        demanded_cargo_token_word_hex_by_path: BTreeMap::from([(
            "book00.line00".to_string(),
            "0x00000000".to_string(),
        )]),
    };

    let differences = diff_recipe_book_line_samples(&[sample_a, sample_b])
        .expect("recipe-book diff should succeed");

    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.profile_family")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.line_kind_by_path.book00.line00")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.mode_word_hex_by_path.book00.line00")
    );
    assert!(
        differences.iter().any(
            |entry| entry.field_path == "$.supplied_cargo_token_word_hex_by_path.book00.line00"
        )
    );
}

#[test]
fn recipe_book_content_diff_ignores_wrapper_metadata() {
    let sample_a = RuntimeRecipeBookLineSample {
        path: "a.gmp".to_string(),
        profile_family: "rt3-105-map-container-v1".to_string(),
        source_kind: "recipe-book-summary".to_string(),
        book_count: 12,
        book_stride_hex: "0x4e1".to_string(),
        line_count: 5,
        line_stride_hex: "0x30".to_string(),
        book_head_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
        book_line_area_kind_by_index: BTreeMap::from([("book00".to_string(), "mixed".to_string())]),
        max_annual_production_word_hex_by_book: BTreeMap::from([(
            "book00".to_string(),
            "0x00000000".to_string(),
        )]),
        line_kind_by_path: BTreeMap::from([("book00.line02".to_string(), "mixed".to_string())]),
        mode_word_hex_by_path: BTreeMap::from([(
            "book00.line02".to_string(),
            "0x00110000".to_string(),
        )]),
        annual_amount_word_hex_by_path: BTreeMap::from([(
            "book00.line02".to_string(),
            "0x00000000".to_string(),
        )]),
        supplied_cargo_token_word_hex_by_path: BTreeMap::from([(
            "book00.line02".to_string(),
            "0x000040a0".to_string(),
        )]),
        demanded_cargo_token_word_hex_by_path: BTreeMap::from([(
            "book00.line01".to_string(),
            "0x72470000".to_string(),
        )]),
    };
    let mut sample_b = sample_a.clone();
    sample_b.path = "b.gms".to_string();
    sample_b.profile_family = "rt3-105-save-container-v1".to_string();
    sample_b.source_kind = "recipe-book-summary".to_string();

    let differences = diff_recipe_book_line_samples(&[sample_a.clone(), sample_b.clone()])
        .expect("wrapper-aware diff should succeed");
    let content_differences = diff_recipe_book_line_content_samples(&[sample_a, sample_b])
        .expect("content diff should succeed");

    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.profile_family")
    );
    assert!(content_differences.is_empty());
}

#[test]
fn diffs_setup_payload_core_samples_across_multiple_files() {
    let sample_a = RuntimeSetupPayloadCoreSample {
        path: "a.gmp".to_string(),
        file_extension: "gmp".to_string(),
        inferred_profile_family: "rt3-105-map-container-v1".to_string(),
        payload_word_0x14: 0x0001,
        payload_word_0x14_hex: "0x0001".to_string(),
        payload_byte_0x20: 0x05,
        payload_byte_0x20_hex: "0x05".to_string(),
        marker_bytes_0x2c9_0x2d0_hex: "0000000000000000".to_string(),
        row_category_byte_0x31a: 0x00,
        row_category_byte_0x31a_hex: "0x00".to_string(),
        row_visibility_byte_0x31b: 0x00,
        row_visibility_byte_0x31b_hex: "0x00".to_string(),
        row_visibility_byte_0x31c: 0x00,
        row_visibility_byte_0x31c_hex: "0x00".to_string(),
        row_count_word_0x3ae: 0x0186,
        row_count_word_0x3ae_hex: "0x0186".to_string(),
        payload_word_0x3b2: 0x0001,
        payload_word_0x3b2_hex: "0x0001".to_string(),
        payload_word_0x3ba: 0x0001,
        payload_word_0x3ba_hex: "0x0001".to_string(),
        candidate_header_word_0_hex: Some("0x10000000".to_string()),
        candidate_header_word_1_hex: Some("0x00009000".to_string()),
    };

    let sample_b = RuntimeSetupPayloadCoreSample {
        path: "b.gms".to_string(),
        file_extension: "gms".to_string(),
        inferred_profile_family: "rt3-105-scenario-save-container-v1".to_string(),
        payload_word_0x14: 0x0001,
        payload_word_0x14_hex: "0x0001".to_string(),
        payload_byte_0x20: 0x05,
        payload_byte_0x20_hex: "0x05".to_string(),
        marker_bytes_0x2c9_0x2d0_hex: "0000000000000000".to_string(),
        row_category_byte_0x31a: 0x00,
        row_category_byte_0x31a_hex: "0x00".to_string(),
        row_visibility_byte_0x31b: 0x00,
        row_visibility_byte_0x31b_hex: "0x00".to_string(),
        row_visibility_byte_0x31c: 0x00,
        row_visibility_byte_0x31c_hex: "0x00".to_string(),
        row_count_word_0x3ae: 0x0186,
        row_count_word_0x3ae_hex: "0x0186".to_string(),
        payload_word_0x3b2: 0x0006,
        payload_word_0x3b2_hex: "0x0006".to_string(),
        payload_word_0x3ba: 0x0001,
        payload_word_0x3ba_hex: "0x0001".to_string(),
        candidate_header_word_0_hex: Some("0x00000000".to_string()),
        candidate_header_word_1_hex: Some("0x00000000".to_string()),
    };

    let differences =
        diff_setup_payload_core_samples(&[sample_a, sample_b]).expect("diff should succeed");

    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.file_extension")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.inferred_profile_family")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.payload_word_0x3b2")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.candidate_header_word_0_hex")
    );
}

#[test]
fn diffs_setup_launch_payload_samples_across_multiple_files() {
    let sample_a = RuntimeSetupLaunchPayloadSample {
        path: "a.gmp".to_string(),
        file_extension: "gmp".to_string(),
        inferred_profile_family: "rt3-105-map-container-v1".to_string(),
        launch_flag_byte_0x22: 0x53,
        launch_flag_byte_0x22_hex: "0x53".to_string(),
        campaign_progress_in_known_range: false,
        campaign_progress_scenario_name: None,
        campaign_progress_page_index: None,
        launch_selector_byte_0x33: 0x00,
        launch_selector_byte_0x33_hex: "0x00".to_string(),
        launch_token_block_0x23_0x32_hex: "01311154010000000000000000000000".to_string(),
        campaign_selector_values: BTreeMap::from([
            ("Go West!".to_string(), 0x01),
            ("Germantown".to_string(), 0x31),
        ]),
        nonzero_campaign_selector_values: BTreeMap::from([
            ("Go West!".to_string(), 0x01),
            ("Germantown".to_string(), 0x31),
        ]),
    };

    let sample_b = RuntimeSetupLaunchPayloadSample {
        path: "b.gms".to_string(),
        file_extension: "gms".to_string(),
        inferred_profile_family: "rt3-105-save-container-v1".to_string(),
        launch_flag_byte_0x22: 0xae,
        launch_flag_byte_0x22_hex: "0xae".to_string(),
        campaign_progress_in_known_range: false,
        campaign_progress_scenario_name: None,
        campaign_progress_page_index: None,
        launch_selector_byte_0x33: 0x00,
        launch_selector_byte_0x33_hex: "0x00".to_string(),
        launch_token_block_0x23_0x32_hex: "01439aae010000000000000000000000".to_string(),
        campaign_selector_values: BTreeMap::from([
            ("Go West!".to_string(), 0x01),
            ("Germantown".to_string(), 0x43),
        ]),
        nonzero_campaign_selector_values: BTreeMap::from([
            ("Go West!".to_string(), 0x01),
            ("Germantown".to_string(), 0x43),
        ]),
    };

    let differences =
        diff_setup_launch_payload_samples(&[sample_a, sample_b]).expect("diff should succeed");

    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.file_extension")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.inferred_profile_family")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.launch_flag_byte_0x22")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.launch_token_block_0x23_0x32_hex")
    );
    assert!(
        differences
            .iter()
            .any(|entry| entry.field_path == "$.campaign_selector_values.Germantown")
    );
}
