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

use crate::package::{source_package_contexts, source_package_for_path, source_package_name};
use crate::{SourcePackageContext, scan_rust_files, source_package_contexts_from_sources};

use super::temp_root;

#[test]
fn scan_rust_files_adds_source_package_context_from_manifest() {
    let root = temp_root("source-package");
    let crate_dir = root.join("crates").join("parser");
    fs::create_dir_all(crate_dir.join("src"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("crate dir: {err}")));
    fs::write(
        crate_dir.join("Cargo.toml"),
        "[package]\nname = \"parser\"\nversion = \"0.1.0\"\n",
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("manifest write: {err}")));
    fs::write(
        crate_dir.join("src").join("lib.rs"),
        "fn load(value: Option<u8>) -> u8 { value.unwrap() }\n",
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("rust write: {err}")));
    let files = vec![
        PathBuf::from("crates/parser/Cargo.toml"),
        PathBuf::from("crates/parser/src/lib.rs"),
    ];
    assert_eq!(
        source_package_name("[package]\nname = \"parser\"\n"),
        Some("parser".to_string())
    );
    let packages = source_package_contexts(&root, &files)
        .unwrap_or_else(|err| std::panic::panic_any(format!("package contexts: {err}")));
    assert_eq!(
        packages,
        vec![SourcePackageContext {
            root: "crates/parser".to_string(),
            name: "parser".to_string()
        }]
    );
    assert!(source_package_for_path(&files[1], &packages).is_some());

    let findings = scan_rust_files(&root, &files)
        .unwrap_or_else(|err| std::panic::panic_any(format!("scan rust files: {err}")));

    let unwrap = findings
        .iter()
        .find(|finding| finding.family.as_deref() == Some("unwrap"))
        .unwrap_or_else(|| std::panic::panic_any("expected unwrap finding"));
    assert_eq!(unwrap.identity.crate_name.as_deref(), Some("parser"));
    fs::remove_dir_all(root).unwrap_or_else(|err| std::panic::panic_any(format!("cleanup: {err}")));
}

#[test]
fn source_package_context_prefers_nested_manifest() {
    let packages = source_package_contexts_from_sources([
        (
            PathBuf::from("Cargo.toml"),
            "[package]\nname = \"root\"\n".to_string(),
        ),
        (
            PathBuf::from("crates/parser/Cargo.toml"),
            "[package]\nname = \"parser\"\n".to_string(),
        ),
    ]);

    let package = source_package_for_path(Path::new("crates/parser/src/lib.rs"), &packages)
        .unwrap_or_else(|| std::panic::panic_any("expected nested package context"));

    assert_eq!(package.name, "parser");
}

#[test]
fn source_package_context_does_not_match_sibling_prefixes() {
    let packages = source_package_contexts_from_sources([(
        PathBuf::from("crates/parser/Cargo.toml"),
        "[package]\nname = \"parser\"\n".to_string(),
    )]);

    assert!(
        source_package_for_path(Path::new("crates/parser-extra/src/lib.rs"), &packages).is_none()
    );
}

#[test]
fn scan_rust_files_ignores_workspace_manifest_without_package_name() {
    let root = temp_root("workspace-manifest");
    fs::create_dir_all(root.join("src"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("src dir: {err}")));
    fs::write(root.join("Cargo.toml"), "[workspace]\nmembers = []\n")
        .unwrap_or_else(|err| std::panic::panic_any(format!("manifest write: {err}")));
    fs::write(
        root.join("src").join("lib.rs"),
        "fn load(value: Option<u8>) -> u8 { value.unwrap() }\n",
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("rust write: {err}")));
    let files = vec![PathBuf::from("Cargo.toml"), PathBuf::from("src/lib.rs")];

    let findings = scan_rust_files(&root, &files)
        .unwrap_or_else(|err| std::panic::panic_any(format!("scan rust files: {err}")));

    let unwrap = findings
        .iter()
        .find(|finding| finding.family.as_deref() == Some("unwrap"))
        .unwrap_or_else(|| std::panic::panic_any("expected unwrap finding"));
    assert_eq!(unwrap.identity.crate_name, None);
    fs::remove_dir_all(root).unwrap_or_else(|err| std::panic::panic_any(format!("cleanup: {err}")));
}

#[test]
fn scan_rust_files_ignores_invalid_manifest_source_text() {
    let root = temp_root("invalid-manifest");
    fs::create_dir_all(root.join("src"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("src dir: {err}")));
    fs::write(root.join("Cargo.toml"), "[package\nname = \"broken\"\n")
        .unwrap_or_else(|err| std::panic::panic_any(format!("manifest write: {err}")));
    fs::write(
        root.join("src").join("lib.rs"),
        "fn load(value: Option<u8>) -> u8 { value.unwrap() }\n",
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("rust write: {err}")));
    let files = vec![PathBuf::from("Cargo.toml"), PathBuf::from("src/lib.rs")];

    let findings = scan_rust_files(&root, &files)
        .unwrap_or_else(|err| std::panic::panic_any(format!("scan rust files: {err}")));

    let unwrap = findings
        .iter()
        .find(|finding| finding.family.as_deref() == Some("unwrap"))
        .unwrap_or_else(|| std::panic::panic_any("expected unwrap finding"));
    assert_eq!(unwrap.identity.crate_name, None);
    fs::remove_dir_all(root).unwrap_or_else(|err| std::panic::panic_any(format!("cleanup: {err}")));
}

#[test]
fn scan_rust_files_ignores_non_utf8_manifest_source_text() {
    let root = temp_root("non-utf8-manifest");
    fs::create_dir_all(root.join("src"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("src dir: {err}")));
    fs::write(
        root.join("Cargo.toml"),
        b"[package]\nname = \"broken\"\n\xFF",
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("manifest write: {err}")));
    fs::write(
        root.join("src").join("lib.rs"),
        "fn load(value: Option<u8>) -> u8 { value.unwrap() }\n",
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("rust write: {err}")));
    let files = vec![PathBuf::from("Cargo.toml"), PathBuf::from("src/lib.rs")];

    let findings = scan_rust_files(&root, &files)
        .unwrap_or_else(|err| std::panic::panic_any(format!("scan rust files: {err}")));

    let unwrap = findings
        .iter()
        .find(|finding| finding.family.as_deref() == Some("unwrap"))
        .unwrap_or_else(|| std::panic::panic_any("expected unwrap finding"));
    assert_eq!(unwrap.identity.crate_name, None);
    fs::remove_dir_all(root).unwrap_or_else(|err| std::panic::panic_any(format!("cleanup: {err}")));
}

#[test]
fn scan_rust_files_ignores_unreadable_manifest_context() {
    let root = temp_root("unreadable-manifest");
    fs::create_dir_all(root.join("Cargo.toml"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("manifest dir: {err}")));
    fs::create_dir_all(root.join("src"))
        .unwrap_or_else(|err| std::panic::panic_any(format!("src dir: {err}")));
    fs::write(
        root.join("src").join("lib.rs"),
        "fn load(value: Option<u8>) -> u8 { value.unwrap() }\n",
    )
    .unwrap_or_else(|err| std::panic::panic_any(format!("rust write: {err}")));
    let files = vec![PathBuf::from("Cargo.toml"), PathBuf::from("src/lib.rs")];

    let findings = scan_rust_files(&root, &files)
        .unwrap_or_else(|err| std::panic::panic_any(format!("scan rust files: {err}")));

    let unwrap = findings
        .iter()
        .find(|finding| finding.family.as_deref() == Some("unwrap"))
        .unwrap_or_else(|| std::panic::panic_any("expected unwrap finding"));
    assert_eq!(unwrap.identity.crate_name, None);
    fs::remove_dir_all(root).unwrap_or_else(|err| std::panic::panic_any(format!("cleanup: {err}")));
}
