//! Validate the canonical editor sample pack and the complete project dependency graph.

use std::path::PathBuf;

use scene::{
    sample_pack::validate_editor_sample_pack, ProjectValidationReport, ValidationSeverity,
};
use serde::Serialize;

#[derive(Serialize)]
struct ValidateSamplesOutput<'a> {
    manifest_path: &'a str,
    samples: &'a [scene::sample_pack::EditorSampleEntry],
    dependencies: &'a [scene::ProjectDependency],
    findings: &'a [scene::ProjectValidationFinding],
}

fn main() {
    let mut project_root = PathBuf::from(".");
    let mut json = false;
    let mut args = std::env::args().skip(1);
    while let Some(argument) = args.next() {
        match argument.as_str() {
            "--json" => json = true,
            "--project" => {
                let Some(path) = args.next() else {
                    eprintln!("validate-samples: --project requires a path");
                    std::process::exit(2);
                };
                project_root = PathBuf::from(path);
            }
            _ => {
                eprintln!("validate-samples: unknown argument `{argument}`");
                std::process::exit(2);
            }
        }
    }

    let sample_validation = validate_editor_sample_pack(&project_root);
    let mut report = scene::validate_project(&project_root);
    merge_reports(&mut report, &sample_validation.report);

    if json {
        let output = ValidateSamplesOutput {
            manifest_path: &sample_validation.manifest_path,
            samples: &sample_validation.samples,
            dependencies: &report.dependencies,
            findings: &report.findings,
        };
        println!(
            "{}",
            serde_json::to_string_pretty(&output).expect("sample validation report must serialize")
        );
    } else {
        for sample in &sample_validation.samples {
            println!(
                "sample: {} [{}] {} ({})",
                sample.area, sample.id, sample.label, sample.scene
            );
        }
        for finding in &report.findings {
            let actor = finding
                .owner_actor_id
                .as_deref()
                .map(|actor| format!(" actor={actor}"))
                .unwrap_or_default();
            eprintln!(
                "{:?} [{}] {}{}: {}",
                finding.severity, finding.code, finding.source_path, actor, finding.message
            );
        }
        println!(
            "validate-samples: {} samples, {} dependencies, {} findings, {} blocking errors",
            sample_validation.samples.len(),
            report.dependencies.len(),
            report.findings.len(),
            report.blocking_error_count()
        );
    }

    if !report.is_release_ready() {
        std::process::exit(1);
    }
}

fn merge_reports(target: &mut ProjectValidationReport, source: &ProjectValidationReport) {
    target
        .dependencies
        .extend(source.dependencies.iter().cloned());
    target.findings.extend(source.findings.iter().cloned());
    target.dependencies.sort_by(|left, right| {
        (
            &left.owner_path,
            &left.owner_actor_id,
            &left.kind,
            &left.reference,
        )
            .cmp(&(
                &right.owner_path,
                &right.owner_actor_id,
                &right.kind,
                &right.reference,
            ))
    });
    target.dependencies.dedup();
    target.findings.sort_by(|left, right| {
        (
            &left.source_path,
            &left.owner_actor_id,
            &left.code,
            &left.reference,
            &left.message,
            severity_rank(left.severity),
            &left.repair,
        )
            .cmp(&(
                &right.source_path,
                &right.owner_actor_id,
                &right.code,
                &right.reference,
                &right.message,
                severity_rank(right.severity),
                &right.repair,
            ))
    });
    target.findings.dedup();
}

const fn severity_rank(severity: ValidationSeverity) -> u8 {
    match severity {
        ValidationSeverity::Error => 0,
        ValidationSeverity::Warning => 1,
        ValidationSeverity::Info => 2,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use scene::{ProjectDependency, ProjectValidationFinding};

    #[test]
    fn merged_report_is_sorted_and_deduplicated() {
        let dependency = ProjectDependency {
            owner_path: "assets/samples/editor_samples.ron".into(),
            owner_actor_id: None,
            kind: "editor_sample_scene".into(),
            reference: "assets/levels/samples/brush.scn.ron".into(),
        };
        let finding = ProjectValidationFinding {
            severity: ValidationSeverity::Error,
            code: "sample_pack.scene_missing".into(),
            source_path: "assets/samples/editor_samples.ron".into(),
            owner_actor_id: None,
            reference: Some("assets/levels/samples/brush.scn.ron".into()),
            message: "missing".into(),
            repair: "restore".into(),
        };
        let source = ProjectValidationReport {
            dependencies: vec![dependency.clone()],
            findings: vec![finding.clone()],
        };
        let mut target = source.clone();

        merge_reports(&mut target, &source);

        assert_eq!(target.dependencies, vec![dependency]);
        assert_eq!(target.findings, vec![finding]);
    }

    #[test]
    fn merged_json_is_deterministic_across_input_order() {
        let dependency = |reference: &str| ProjectDependency {
            owner_path: "assets/samples/editor_samples.ron".into(),
            owner_actor_id: None,
            kind: "editor_sample_scene".into(),
            reference: reference.into(),
        };
        let finding = |code: &str, repair: &str| ProjectValidationFinding {
            severity: ValidationSeverity::Error,
            code: code.into(),
            source_path: "assets/samples/editor_samples.ron".into(),
            owner_actor_id: None,
            reference: Some(code.into()),
            message: code.into(),
            repair: repair.into(),
        };
        let first = ProjectValidationReport {
            dependencies: vec![dependency("assets/levels/z.scn.ron")],
            findings: vec![finding("sample_pack.same", "repair-z")],
        };
        let second = ProjectValidationReport {
            dependencies: vec![dependency("assets/levels/a.scn.ron")],
            findings: vec![finding("sample_pack.same", "repair-a")],
        };
        let mut left = first.clone();
        merge_reports(&mut left, &second);
        let mut right = second;
        merge_reports(&mut right, &first);

        let left_json = serde_json::to_string(&left).unwrap();
        let right_json = serde_json::to_string(&right).unwrap();

        assert_eq!(left, right);
        assert_eq!(left_json, right_json);
        assert_eq!(left.findings[0].repair, "repair-a");
        assert_eq!(left.findings[1].repair, "repair-z");
    }
}
