use std::{
    collections::{BTreeMap, BTreeSet},
    fs, io,
    path::{Path, PathBuf},
};

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
enum BridgePackage {
    Api,
    Decompile,
    Edit,
    Project,
    Query,
    Runtime,
    Serialization,
    Session,
}

impl BridgePackage {
    fn from_package(package: &str) -> Option<Self> {
        match package {
            "org.ghidra.api" => Some(Self::Api),
            "org.ghidra.bridge.decompile" => Some(Self::Decompile),
            "org.ghidra.bridge.edit" => Some(Self::Edit),
            "org.ghidra.bridge.project" => Some(Self::Project),
            "org.ghidra.bridge.query" => Some(Self::Query),
            "org.ghidra.bridge.runtime" => Some(Self::Runtime),
            "org.ghidra.bridge.serialization" => Some(Self::Serialization),
            "org.ghidra.bridge.session" => Some(Self::Session),
            _ => None,
        }
    }

    fn from_import(import: &str) -> Option<Self> {
        [
            ("org.ghidra.bridge.serialization.", Self::Serialization),
            ("org.ghidra.bridge.decompile.", Self::Decompile),
            ("org.ghidra.bridge.project.", Self::Project),
            ("org.ghidra.bridge.runtime.", Self::Runtime),
            ("org.ghidra.bridge.session.", Self::Session),
            ("org.ghidra.bridge.query.", Self::Query),
            ("org.ghidra.bridge.edit.", Self::Edit),
            ("org.ghidra.api.", Self::Api),
        ]
        .into_iter()
        .find_map(|(prefix, package)| import.starts_with(prefix).then_some(package))
    }

    fn allowed_dependencies(self) -> BTreeSet<Self> {
        match self {
            Self::Api => [
                Self::Decompile,
                Self::Edit,
                Self::Project,
                Self::Query,
                Self::Runtime,
                Self::Serialization,
                Self::Session,
            ]
            .into_iter()
            .collect(),
            Self::Decompile => [
                Self::Query,
                Self::Runtime,
                Self::Serialization,
                Self::Session,
            ]
            .into_iter()
            .collect(),
            Self::Edit => [Self::Query, Self::Session].into_iter().collect(),
            Self::Project => [Self::Runtime, Self::Serialization, Self::Session]
                .into_iter()
                .collect(),
            Self::Query => [Self::Runtime, Self::Serialization].into_iter().collect(),
            Self::Runtime | Self::Serialization => BTreeSet::new(),
            Self::Session => [Self::Serialization].into_iter().collect(),
        }
    }
}

#[test]
fn java_bridge_uses_gradle_source_layout() {
    let bridge_root = repo_root().join("bridge");
    let source_root = java_source_root();

    assert!(
        source_root.is_dir(),
        "Java bridge source root is missing: {}",
        source_root.display()
    );
    assert!(
        !bridge_root.join("org").exists(),
        "legacy Java source root still exists at {}",
        bridge_root.join("org").display()
    );
    assert!(
        bridge_root.join("settings.gradle.kts").is_file(),
        "Gradle settings file is missing"
    );
    assert!(
        bridge_root.join("build.gradle.kts").is_file(),
        "Gradle build file is missing"
    );

    let sources = java_sources(&source_root).expect("discover Java bridge sources");
    assert!(
        sources.len() > 10,
        "Java bridge source tree is unexpectedly sparse: {sources:#?}"
    );
    assert!(
        sources
            .iter()
            .any(|source| source.ends_with("org/ghidra/api/GhidraApiBridge.java")),
        "public facade source is missing from Gradle source layout"
    );
}

#[test]
fn java_generated_outputs_are_ignored() {
    let gitignore = fs::read_to_string(repo_root().join(".gitignore")).expect("read .gitignore");
    for ignored in [
        ".idea/",
        "bridge/.gradle/",
        "bridge/.classpath",
        "bridge/.project",
        "bridge/.settings/",
        "bridge/bin/",
        "bridge/build/",
    ] {
        assert!(
            gitignore.lines().any(|line| line == ignored),
            "{ignored} should stay ignored as generated Java project state"
        );
    }
}

#[test]
fn java_packages_match_source_paths() {
    let source_root = java_source_root();
    let sources = java_sources(&source_root).expect("discover Java bridge sources");

    for source in sources {
        let content = fs::read_to_string(&source)
            .unwrap_or_else(|err| panic!("read {}: {err}", source.display()));
        let declared_package = package_declaration(&content)
            .unwrap_or_else(|| panic!("{} does not declare a package", source.display()));
        let expected_package = package_from_path(&source_root, &source);
        assert_eq!(
            declared_package,
            expected_package,
            "package declaration does not match source path for {}",
            source.display()
        );
    }
}

#[test]
fn java_bridge_public_facade_is_narrow() {
    let source_root = java_source_root();
    let mut public_api_sources = Vec::new();

    for source in java_sources(&source_root).expect("discover Java bridge sources") {
        let content = fs::read_to_string(&source)
            .unwrap_or_else(|err| panic!("read {}: {err}", source.display()));
        if package_declaration(&content) == Some("org.ghidra.api") {
            public_api_sources.push(relative_path(&repo_root(), &source));
        }
    }

    assert_eq!(
        public_api_sources,
        vec![PathBuf::from(
            "bridge/src/main/java/org/ghidra/api/GhidraApiBridge.java"
        )],
        "org.ghidra.api must stay a single public facade"
    );
}

#[test]
fn java_bridge_imports_follow_dependency_direction() {
    let source_root = java_source_root();
    let sources = java_sources(&source_root).expect("discover Java bridge sources");
    let mut violations: BTreeMap<PathBuf, Vec<String>> = BTreeMap::new();

    for source in sources {
        let content = fs::read_to_string(&source)
            .unwrap_or_else(|err| panic!("read {}: {err}", source.display()));
        let Some(package) = package_declaration(&content) else {
            violations
                .entry(relative_path(&repo_root(), &source))
                .or_default()
                .push("missing package declaration".to_owned());
            continue;
        };
        let Some(source_package) = BridgePackage::from_package(package) else {
            violations
                .entry(relative_path(&repo_root(), &source))
                .or_default()
                .push(format!("unexpected bridge package {package:?}"));
            continue;
        };
        let allowed_dependencies = source_package.allowed_dependencies();

        for import in import_declarations(&content) {
            let Some(imported_package) = BridgePackage::from_import(import) else {
                continue;
            };
            if imported_package == source_package {
                continue;
            }
            if !allowed_dependencies.contains(&imported_package) {
                violations
                    .entry(relative_path(&repo_root(), &source))
                    .or_default()
                    .push(format!(
                        "{source_package:?} must not import {imported_package:?}: {import}"
                    ));
            }
        }
    }

    assert!(
        violations.is_empty(),
        "Java bridge package dependency violations:\n{}",
        violations
            .into_iter()
            .map(|(source, messages)| format!("{}:\n  {}", source.display(), messages.join("\n  ")))
            .collect::<Vec<_>>()
            .join("\n")
    );
}

#[test]
fn cargo_build_script_uses_gradle_java_source_root() {
    let build_rs_path = repo_root().join("build.rs");
    let build_rs = fs::read_to_string(&build_rs_path).expect("read build.rs");

    assert!(
        build_rs.contains(r#"const BRIDGE_SOURCE_ROOT: &str = "bridge/src/main/java";"#),
        "build.rs must compile Java from the packaged bridge source root"
    );
    assert!(
        !build_rs.contains(r#"const BRIDGE_SOURCE_ROOT: &str = "bridge";"#),
        "build.rs must not point javac at the bridge project root"
    );
}

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

fn java_source_root() -> PathBuf {
    repo_root().join("bridge/src/main/java")
}

fn java_sources(root: &Path) -> io::Result<Vec<PathBuf>> {
    let mut sources = Vec::new();
    collect_java_sources(root, &mut sources)?;
    sources.sort();
    Ok(sources)
}

fn collect_java_sources(path: &Path, sources: &mut Vec<PathBuf>) -> io::Result<()> {
    let mut entries = fs::read_dir(path)?.collect::<Result<Vec<_>, _>>()?;
    entries.sort_by_key(|entry| entry.path());
    for entry in entries {
        let path = entry.path();
        if path.is_dir() {
            collect_java_sources(&path, sources)?;
        } else if path
            .extension()
            .is_some_and(|extension| extension == "java")
        {
            sources.push(path);
        }
    }
    Ok(())
}

fn package_declaration(content: &str) -> Option<&str> {
    content.lines().find_map(|line| {
        line.trim()
            .strip_prefix("package ")
            .and_then(|value| value.strip_suffix(';'))
    })
}

fn import_declarations(content: &str) -> Vec<&str> {
    content
        .lines()
        .filter_map(|line| {
            line.trim()
                .strip_prefix("import ")
                .map(|value| value.strip_prefix("static ").unwrap_or(value))
                .and_then(|value| value.strip_suffix(';'))
        })
        .collect()
}

fn package_from_path(source_root: &Path, source: &Path) -> String {
    let parent = source
        .parent()
        .unwrap_or_else(|| panic!("{} has no parent directory", source.display()));
    relative_path(source_root, parent)
        .components()
        .map(|component| {
            component
                .as_os_str()
                .to_str()
                .unwrap_or_else(|| panic!("non-UTF-8 Java source path: {}", source.display()))
        })
        .collect::<Vec<_>>()
        .join(".")
}

fn relative_path(root: &Path, path: &Path) -> PathBuf {
    path.strip_prefix(root)
        .unwrap_or_else(|err| panic!("strip {} from {}: {err}", root.display(), path.display()))
        .to_owned()
}
