//! Compile-time-backed audit of every pinned upstream program and `TestClient` command.

use crate::commands::TEST_CLIENT_COMMANDS;
use crate::test_client::{IMPLEMENTED_TEST_CLIENT_COMMANDS, registered_test_client_command_count};
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};

const CARGO_MANIFEST: &str = include_str!("../Cargo.toml");
const SOURCE_MANIFEST: &str = include_str!("../upstream-programs.json");

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProgramTarget {
    pub project: &'static str,
    pub binary: &'static str,
    binary_source: &'static str,
    offline_test_source: &'static str,
}

pub const ORIGINAL_PROGRAMS: &[ProgramTarget] = &[
    ProgramTarget {
        project: "OSDInspector",
        binary: "osd-inspector",
        binary_source: include_str!("bin/osd_inspector.rs"),
        offline_test_source: include_str!("../tests/osd_inspector_cli.rs"),
    },
    ProgramTarget {
        project: "SimpleBot",
        binary: "simple-bot",
        binary_source: include_str!("bin/simple_bot.rs"),
        offline_test_source: include_str!("../tests/simple_bot_cli.rs"),
    },
    ProgramTarget {
        project: "PacketDump",
        binary: "packet-dump",
        binary_source: include_str!("bin/packet_dump.rs"),
        offline_test_source: include_str!("../tests/packet_dump_cli.rs"),
    },
    ProgramTarget {
        project: "PrimInspector",
        binary: "prim-inspector",
        binary_source: include_str!("bin/prim_inspector.rs"),
        offline_test_source: include_str!("../tests/prim_inspector_cli.rs"),
    },
    ProgramTarget {
        project: "InventoryExplorer",
        binary: "inventory-explorer",
        binary_source: include_str!("bin/inventory_explorer.rs"),
        offline_test_source: include_str!("../tests/inventory_explorer_cli.rs"),
    },
    ProgramTarget {
        project: "IRCGateway",
        binary: "irc-gateway",
        binary_source: include_str!("bin/irc_gateway.rs"),
        offline_test_source: include_str!("../tests/irc_gateway_cli.rs"),
    },
    ProgramTarget {
        project: "TestClient",
        binary: "test-client",
        binary_source: include_str!("bin/test_client.rs"),
        offline_test_source: include_str!("../tests/test_client_services_cli.rs"),
    },
    ProgramTarget {
        project: "VivoxTest",
        binary: "vivox-test",
        binary_source: include_str!("bin/vivox_test.rs"),
        offline_test_source: include_str!("../tests/vivox_test_cli.rs"),
    },
    ProgramTarget {
        project: "WebRtcTest",
        binary: "webrtc-test",
        binary_source: include_str!("bin/webrtc_test.rs"),
        offline_test_source: include_str!("../tests/webrtc_test_cli.rs"),
    },
];

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CompletenessReport {
    pub programs: usize,
    pub source_files: usize,
    pub test_client_commands: usize,
    pub pending_programs: usize,
    pub pending_commands: usize,
}

#[derive(Deserialize)]
struct SourceManifest {
    upstream_commit: String,
    projects: Vec<SourceProject>,
}

#[derive(Deserialize)]
struct SourceProject {
    project: String,
    files: Vec<SourceFile>,
}

#[derive(Deserialize)]
struct SourceFile {
    path: String,
    sha256: String,
}

/// Audits the pinned source manifest, all original Rust program targets, their
/// offline test entry points, and every generated `TestClient` command.
///
/// # Errors
///
/// Returns an error when the pinned source manifest is malformed or differs
/// from the native target, test, implementation, or runtime registries.
pub fn audit_program_completeness() -> Result<CompletenessReport, String> {
    let manifest: SourceManifest = serde_json::from_str(SOURCE_MANIFEST)
        .map_err(|_| "program source manifest is not valid JSON".to_owned())?;
    if manifest.upstream_commit.len() != 40
        || !manifest
            .upstream_commit
            .bytes()
            .all(|byte| byte.is_ascii_hexdigit())
    {
        return Err("program source manifest has an invalid upstream commit".into());
    }
    let mut projects = BTreeMap::new();
    let mut source_paths = BTreeSet::new();
    let mut source_files = 0_usize;
    for project in &manifest.projects {
        if project.files.is_empty() || projects.insert(project.project.as_str(), project).is_some()
        {
            return Err("program source manifest has an empty or duplicate project".into());
        }
        for file in &project.files {
            if !source_paths.insert(file.path.as_str())
                || file.sha256.len() != 64
                || !file.sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
            {
                return Err("program source manifest has an invalid file record".into());
            }
            source_files += 1;
        }
    }
    let expected_projects: BTreeSet<_> = ORIGINAL_PROGRAMS
        .iter()
        .map(|target| target.project)
        .collect();
    if projects.keys().copied().collect::<BTreeSet<_>>() != expected_projects {
        return Err("Rust program targets do not exactly match the source manifest".into());
    }
    for target in ORIGINAL_PROGRAMS {
        let cargo_marker = format!("name = \"{}\"", target.binary);
        if !CARGO_MANIFEST.contains(&cargo_marker)
            || target.binary_source.contains("pending_program")
            || !target.binary_source.contains("main_entry")
            || !target.offline_test_source.contains("#[test]")
        {
            return Err(format!(
                "program {} lacks a native target or offline test",
                target.project
            ));
        }
    }

    let source_commands: BTreeSet<_> = projects["TestClient"]
        .files
        .iter()
        .filter_map(|file| {
            file.path
                .contains("/Commands/")
                .then(|| file.path.rsplit('/').next())
                .flatten()
                .and_then(|name| name.strip_suffix(".cs"))
        })
        .collect();
    let generated_commands: BTreeSet<_> = TEST_CLIENT_COMMANDS.iter().copied().collect();
    let implemented_commands: BTreeSet<_> =
        IMPLEMENTED_TEST_CLIENT_COMMANDS.iter().copied().collect();
    if source_commands != generated_commands
        || implemented_commands != generated_commands
        || registered_test_client_command_count() != generated_commands.len()
    {
        return Err("TestClient source, implementation, and runtime registries differ".into());
    }

    Ok(CompletenessReport {
        programs: ORIGINAL_PROGRAMS.len(),
        source_files,
        test_client_commands: generated_commands.len(),
        pending_programs: 0,
        pending_commands: 0,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_source_program_and_command_has_a_native_tested_target() {
        let report = audit_program_completeness().unwrap();
        assert_eq!(report.programs, 9);
        assert!(report.source_files > report.programs);
        assert_eq!(report.test_client_commands, TEST_CLIENT_COMMANDS.len());
        assert_eq!(report.pending_programs, 0);
        assert_eq!(report.pending_commands, 0);
    }
}
