use std::path::{Path, PathBuf};

use rrt_runtime::{
    load_runtime_save_slice_document, load_runtime_snapshot_document, load_runtime_state_import,
    project_save_slice_to_runtime_state_import, validate_runtime_save_slice_document,
    validate_runtime_snapshot_document,
};

use crate::{FixtureDocument, FixtureStateOrigin, RawFixtureDocument};

pub fn load_fixture_document(path: &Path) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
    let text = std::fs::read_to_string(path)?;
    let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
    load_fixture_document_from_str_with_base(&text, base_dir)
}

pub fn load_fixture_document_from_str(
    text: &str,
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
    load_fixture_document_from_str_with_base(text, Path::new("."))
}

pub fn load_fixture_document_from_str_with_base(
    text: &str,
    base_dir: &Path,
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
    let raw: RawFixtureDocument = serde_json::from_str(text)?;
    resolve_raw_fixture_document(raw, base_dir)
}

fn resolve_raw_fixture_document(
    raw: RawFixtureDocument,
    base_dir: &Path,
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
    let specified_state_inputs = usize::from(raw.state.is_some())
        + usize::from(raw.state_snapshot_path.is_some())
        + usize::from(raw.state_save_slice_path.is_some())
        + usize::from(raw.state_import_path.is_some());
    if specified_state_inputs != 1 {
        return Err(
            "fixture must specify exactly one of inline state, state_snapshot_path, state_save_slice_path, or state_import_path"
                .into(),
        );
    }

    let state = match (
        &raw.state,
        &raw.state_snapshot_path,
        &raw.state_save_slice_path,
        &raw.state_import_path,
    ) {
        (Some(state), None, None, None) => state.clone(),
        (None, Some(snapshot_path), None, None) => {
            let snapshot_path = resolve_snapshot_path(base_dir, snapshot_path);
            let snapshot = load_runtime_snapshot_document(&snapshot_path)?;
            validate_runtime_snapshot_document(&snapshot).map_err(|err| {
                format!(
                    "invalid runtime snapshot {}: {err}",
                    snapshot_path.display()
                )
            })?;
            snapshot.state
        }
        (None, None, Some(save_slice_path), None) => {
            let save_slice_path = resolve_snapshot_path(base_dir, save_slice_path);
            let document = load_runtime_save_slice_document(&save_slice_path)?;
            validate_runtime_save_slice_document(&document).map_err(|err| {
                format!(
                    "invalid runtime save slice document {}: {err}",
                    save_slice_path.display()
                )
            })?;
            project_save_slice_to_runtime_state_import(
                &document.save_slice,
                &document.save_slice_id,
                document.source.description.clone(),
            )
            .map_err(|err| {
                format!(
                    "failed to project runtime save slice document {}: {err}",
                    save_slice_path.display()
                )
            })?
            .state
        }
        (None, None, None, Some(import_path)) => {
            let import_path = resolve_snapshot_path(base_dir, import_path);
            load_runtime_state_import(&import_path)
                .map_err(|err| {
                    format!(
                        "failed to load runtime import {}: {err}",
                        import_path.display()
                    )
                })?
                .state
        }
        _ => unreachable!("state input exclusivity checked above"),
    };

    let state_origin = match (
        raw.state_snapshot_path,
        raw.state_save_slice_path,
        raw.state_import_path,
    ) {
        (Some(snapshot_path), None, None) => FixtureStateOrigin::SnapshotPath(snapshot_path),
        (None, Some(save_slice_path), None) => FixtureStateOrigin::SaveSlicePath(save_slice_path),
        (None, None, Some(import_path)) => FixtureStateOrigin::ImportPath(import_path),
        _ => FixtureStateOrigin::Inline,
    };

    Ok(FixtureDocument {
        format_version: raw.format_version,
        fixture_id: raw.fixture_id,
        source: raw.source,
        state,
        state_origin,
        commands: raw.commands,
        expected_summary: raw.expected_summary,
        expected_state_fragment: raw.expected_state_fragment,
    })
}

fn resolve_snapshot_path(base_dir: &Path, snapshot_path: &str) -> PathBuf {
    let candidate = PathBuf::from(snapshot_path);
    if candidate.is_absolute() {
        candidate
    } else {
        base_dir.join(candidate)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::FixtureStateOrigin;
    use rrt_runtime::{
        CalendarPoint, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
        RuntimeOverlayImportDocumentSource, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
        RuntimeSaveSliceDocumentSource, RuntimeServiceState, RuntimeSnapshotDocument,
        RuntimeSnapshotSource, RuntimeState, RuntimeTrackPieceCounts, RuntimeWorldRestoreState,
        SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION,
        save_runtime_overlay_import_document, save_runtime_save_slice_document,
        save_runtime_snapshot_document,
    };
    use std::collections::BTreeMap;

    #[test]
    fn loads_fixture_from_relative_snapshot_path() {
        let nonce = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("system time should be after epoch")
            .as_nanos();
        let fixture_dir = std::env::temp_dir().join(format!("rrt-fixture-load-{nonce}"));
        std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created");

        let snapshot_path = fixture_dir.join("state.json");
        let snapshot = RuntimeSnapshotDocument {
            format_version: SNAPSHOT_FORMAT_VERSION,
            snapshot_id: "snapshot-backed-fixture-state".to_string(),
            source: RuntimeSnapshotSource {
                source_fixture_id: Some("snapshot-backed-fixture".to_string()),
                description: Some("test snapshot".to_string()),
            },
            state: RuntimeState {
                calendar: CalendarPoint {
                    year: 1830,
                    month_slot: 0,
                    phase_slot: 0,
                    tick_slot: 5,
                },
                world_flags: BTreeMap::new(),
                save_profile: RuntimeSaveProfileState::default(),
                world_restore: RuntimeWorldRestoreState::default(),
                metadata: BTreeMap::new(),
                companies: Vec::new(),
                selected_company_id: None,
                players: Vec::new(),
                selected_player_id: None,
                chairman_profiles: Vec::new(),
                selected_chairman_profile_id: None,
                trains: Vec::new(),
                locomotive_catalog: Vec::new(),
                cargo_catalog: Vec::new(),
                territories: Vec::new(),
                company_territory_track_piece_counts: Vec::new(),
                company_territory_access: Vec::new(),
                packed_event_collection: None,
                event_runtime_records: Vec::new(),
                candidate_availability: BTreeMap::new(),
                named_locomotive_availability: BTreeMap::new(),
                named_locomotive_cost: BTreeMap::new(),
                all_cargo_price_override: None,
                named_cargo_price_overrides: BTreeMap::new(),
                all_cargo_production_override: None,
                factory_cargo_production_override: None,
                farm_mine_cargo_production_override: None,
                named_cargo_production_overrides: BTreeMap::new(),
                cargo_production_overrides: BTreeMap::new(),
                world_runtime_variables: BTreeMap::new(),
                company_runtime_variables: BTreeMap::new(),
                player_runtime_variables: BTreeMap::new(),
                territory_runtime_variables: BTreeMap::new(),
                world_scalar_overrides: BTreeMap::new(),
                special_conditions: BTreeMap::new(),
                service_state: RuntimeServiceState::default(),
            },
        };
        save_runtime_snapshot_document(&snapshot_path, &snapshot).expect("snapshot should save");

        let fixture_json = r#"
{
  "format_version": 1,
  "fixture_id": "snapshot-backed-fixture",
  "source": {
    "kind": "captured-runtime"
  },
  "state_snapshot_path": "state.json",
  "commands": [
    {
      "kind": "step_count",
      "steps": 1
    }
  ],
  "expected_summary": {
    "calendar": {
      "year": 1830,
      "month_slot": 0,
      "phase_slot": 0,
      "tick_slot": 6
    },
    "world_flag_count": 0,
    "company_count": 0,
    "event_runtime_record_count": 0,
    "total_company_cash": 0
  }
}
"#;

        let fixture = load_fixture_document_from_str_with_base(fixture_json, &fixture_dir)
            .expect("snapshot-backed fixture should load");

        assert_eq!(
            fixture.state_origin,
            FixtureStateOrigin::SnapshotPath("state.json".to_string())
        );
        assert_eq!(fixture.state.calendar.tick_slot, 5);

        let _ = std::fs::remove_file(snapshot_path);
        let _ = std::fs::remove_dir(fixture_dir);
    }

    #[test]
    fn loads_fixture_from_relative_save_slice_path() {
        let nonce = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("system time should be after epoch")
            .as_nanos();
        let fixture_dir = std::env::temp_dir().join(format!("rrt-fixture-save-slice-{nonce}"));
        std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created");

        let save_slice_path = fixture_dir.join("state-save-slice.json");
        let save_slice = RuntimeSaveSliceDocument {
            format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
            save_slice_id: "save-slice-backed-fixture-state".to_string(),
            source: RuntimeSaveSliceDocumentSource {
                description: Some("test save slice".to_string()),
                original_save_filename: Some("fixture.gms".to_string()),
                original_save_sha256: None,
                notes: vec![],
            },
            save_slice: rrt_runtime::SmpLoadedSaveSlice {
                file_extension_hint: Some("gms".to_string()),
                container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
                mechanism_family: "classic-save-rehydrate-v1".to_string(),
                mechanism_confidence: "grounded".to_string(),
                trailer_family: None,
                bridge_family: None,
                profile: None,
                candidate_availability_table: None,
                named_locomotive_availability_table: None,
                locomotive_catalog: None,
                cargo_catalog: None,
                world_issue_37_state: None,
                world_economic_tuning_state: None,
                world_finance_neighborhood_state: None,
                world_locomotive_policy_state: None,
                company_roster: None,
                chairman_profile_table: None,
                region_collection: None,
                region_fixed_row_run_summary: None,
                placed_structure_collection: None,
                placed_structure_dynamic_side_buffer_summary: None,
                special_conditions_table: None,
                event_runtime_collection: None,
                notes: vec![],
            },
        };
        save_runtime_save_slice_document(&save_slice_path, &save_slice)
            .expect("save slice should save");

        let fixture_json = r#"
{
  "format_version": 1,
  "fixture_id": "save-slice-backed-fixture",
  "source": {
    "kind": "captured-runtime"
  },
  "state_save_slice_path": "state-save-slice.json",
  "commands": [],
  "expected_summary": {
    "calendar_projection_is_placeholder": true,
    "packed_event_collection_present": false
  }
}
"#;

        let fixture = load_fixture_document_from_str_with_base(fixture_json, &fixture_dir)
            .expect("save-slice-backed fixture should load");

        assert_eq!(
            fixture.state_origin,
            FixtureStateOrigin::SaveSlicePath("state-save-slice.json".to_string())
        );
        assert_eq!(
            fixture
                .state
                .metadata
                .get("save_slice.import_projection")
                .map(String::as_str),
            Some("partial-runtime-restore-v1")
        );

        let _ = std::fs::remove_file(save_slice_path);
        let _ = std::fs::remove_dir(fixture_dir);
    }

    #[test]
    fn loads_fixture_from_relative_import_path() {
        let nonce = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("system time should be after epoch")
            .as_nanos();
        let fixture_dir = std::env::temp_dir().join(format!("rrt-fixture-import-{nonce}"));
        std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created");

        let snapshot_path = fixture_dir.join("base.json");
        let save_slice_path = fixture_dir.join("slice.json");
        let import_path = fixture_dir.join("overlay.json");

        let snapshot = RuntimeSnapshotDocument {
            format_version: SNAPSHOT_FORMAT_VERSION,
            snapshot_id: "base".to_string(),
            source: RuntimeSnapshotSource::default(),
            state: RuntimeState {
                calendar: CalendarPoint {
                    year: 1830,
                    month_slot: 0,
                    phase_slot: 0,
                    tick_slot: 5,
                },
                world_flags: BTreeMap::new(),
                save_profile: RuntimeSaveProfileState::default(),
                world_restore: RuntimeWorldRestoreState::default(),
                metadata: BTreeMap::new(),
                companies: vec![rrt_runtime::RuntimeCompany {
                    company_id: 42,
                    controller_kind: rrt_runtime::RuntimeCompanyControllerKind::Human,
                    current_cash: 100,
                    debt: 0,
                    credit_rating_score: None,
                    prime_rate: None,
                    track_piece_counts: RuntimeTrackPieceCounts::default(),
                    active: true,
                    available_track_laying_capacity: None,
                    linked_chairman_profile_id: None,
                    book_value_per_share: 0,
                    investor_confidence: 0,
                    management_attitude: 0,
                    takeover_cooldown_year: None,
                    merger_cooldown_year: None,
                }],
                selected_company_id: Some(42),
                players: Vec::new(),
                selected_player_id: None,
                chairman_profiles: Vec::new(),
                selected_chairman_profile_id: None,
                trains: Vec::new(),
                locomotive_catalog: Vec::new(),
                cargo_catalog: Vec::new(),
                territories: Vec::new(),
                company_territory_track_piece_counts: Vec::new(),
                company_territory_access: Vec::new(),
                packed_event_collection: None,
                event_runtime_records: Vec::new(),
                candidate_availability: BTreeMap::new(),
                named_locomotive_availability: BTreeMap::new(),
                named_locomotive_cost: BTreeMap::new(),
                all_cargo_price_override: None,
                named_cargo_price_overrides: BTreeMap::new(),
                all_cargo_production_override: None,
                factory_cargo_production_override: None,
                farm_mine_cargo_production_override: None,
                named_cargo_production_overrides: BTreeMap::new(),
                cargo_production_overrides: BTreeMap::new(),
                world_runtime_variables: BTreeMap::new(),
                company_runtime_variables: BTreeMap::new(),
                player_runtime_variables: BTreeMap::new(),
                territory_runtime_variables: BTreeMap::new(),
                world_scalar_overrides: BTreeMap::new(),
                special_conditions: BTreeMap::new(),
                service_state: RuntimeServiceState::default(),
            },
        };
        save_runtime_snapshot_document(&snapshot_path, &snapshot).expect("snapshot should save");

        let save_slice = RuntimeSaveSliceDocument {
            format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
            save_slice_id: "slice".to_string(),
            source: RuntimeSaveSliceDocumentSource::default(),
            save_slice: rrt_runtime::SmpLoadedSaveSlice {
                file_extension_hint: Some("gms".to_string()),
                container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
                mechanism_family: "classic-save-rehydrate-v1".to_string(),
                mechanism_confidence: "grounded".to_string(),
                trailer_family: None,
                bridge_family: None,
                profile: None,
                candidate_availability_table: None,
                named_locomotive_availability_table: None,
                locomotive_catalog: None,
                cargo_catalog: None,
                world_issue_37_state: None,
                world_economic_tuning_state: None,
                world_finance_neighborhood_state: None,
                world_locomotive_policy_state: None,
                company_roster: None,
                chairman_profile_table: None,
                region_collection: None,
                region_fixed_row_run_summary: None,
                placed_structure_collection: None,
                placed_structure_dynamic_side_buffer_summary: None,
                special_conditions_table: None,
                event_runtime_collection: Some(
                    rrt_runtime::SmpLoadedEventRuntimeCollectionSummary {
                        source_kind: "packed-event-runtime-collection".to_string(),
                        mechanism_family: "classic-save-rehydrate-v1".to_string(),
                        mechanism_confidence: "grounded".to_string(),
                        container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
                        metadata_tag_offset: 0x7100,
                        records_tag_offset: 0x7200,
                        close_tag_offset: 0x7600,
                        packed_state_version: 0x3e9,
                        packed_state_version_hex: "0x000003e9".to_string(),
                        live_id_bound: 7,
                        live_record_count: 1,
                        live_entry_ids: vec![7],
                        decoded_record_count: 1,
                        imported_runtime_record_count: 0,
                        records_with_trigger_kind: 0,
                        records_missing_trigger_kind: 0,
                        nondirect_compact_record_count: 0,
                        nondirect_compact_records_missing_trigger_kind: 0,
                        trigger_kinds_present: vec![],
                        add_building_dispatch_strip_record_indexes: vec![],
                        add_building_dispatch_strip_descriptor_labels: vec![],
                        add_building_dispatch_strip_records_with_trigger_kind: 0,
                        add_building_dispatch_strip_records_missing_trigger_kind: 0,
                        add_building_dispatch_strip_row_shape_families: vec![],
                        add_building_dispatch_strip_signature_families: vec![],
                        add_building_dispatch_strip_condition_tuple_families: vec![],
                        add_building_dispatch_strip_signature_condition_clusters: vec![],
                        control_lane_notes: vec![],
                        records: vec![rrt_runtime::SmpLoadedPackedEventRecordSummary {
                            record_index: 0,
                            live_entry_id: 7,
                            payload_offset: Some(0x7202),
                            payload_len: Some(48),
                            decode_status: "parity_only".to_string(),
                            payload_family: "synthetic_harness".to_string(),
                            trigger_kind: Some(7),
                            active: Some(true),
                            marks_collection_dirty: Some(false),
                            one_shot: Some(false),
                            compact_control: None,
                            text_bands: vec![],
                            standalone_condition_row_count: 0,
                            standalone_condition_rows: vec![],
                            negative_sentinel_scope: None,
                            grouped_effect_row_counts: vec![0, 0, 0, 0],
                            grouped_effect_rows: vec![],
                            decoded_conditions: Vec::new(),
                            decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash {
                                target: rrt_runtime::RuntimeCompanyTarget::Ids { ids: vec![42] },
                                delta: 25,
                            }],
                            executable_import_ready: false,
                            notes: vec![],
                        }],
                    },
                ),
                notes: vec![],
            },
        };
        save_runtime_save_slice_document(&save_slice_path, &save_slice)
            .expect("save slice should save");

        let overlay = RuntimeOverlayImportDocument {
            format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION,
            import_id: "overlay-backed-fixture-state".to_string(),
            source: RuntimeOverlayImportDocumentSource::default(),
            base_snapshot_path: "base.json".to_string(),
            save_slice_path: "slice.json".to_string(),
        };
        save_runtime_overlay_import_document(&import_path, &overlay)
            .expect("overlay import should save");

        let fixture_json = r#"
{
  "format_version": 1,
  "fixture_id": "overlay-backed-fixture",
  "source": {
    "kind": "captured-runtime"
  },
  "state_import_path": "overlay.json",
  "commands": [
    {
      "kind": "service_trigger_kind",
      "trigger_kind": 7
    }
  ],
  "expected_summary": {
    "company_count": 1,
    "packed_event_imported_runtime_record_count": 1
  }
}
"#;

        let fixture = load_fixture_document_from_str_with_base(fixture_json, &fixture_dir)
            .expect("overlay-backed fixture should load");

        assert_eq!(
            fixture.state_origin,
            FixtureStateOrigin::ImportPath("overlay.json".to_string())
        );
        assert_eq!(fixture.state.event_runtime_records.len(), 1);

        let _ = std::fs::remove_file(snapshot_path);
        let _ = std::fs::remove_file(save_slice_path);
        let _ = std::fs::remove_file(import_path);
        let _ = std::fs::remove_dir(fixture_dir);
    }
}
