use std::fs;

use opi_coding_agent::package_resolver::{
    InstalledPackageScope, PackageDiagnosticSeverity, manifest_sha256, resolve_installed_packages,
    source_identity_for_resolution,
};
use opi_coding_agent::package_store::{
    PackageDeclaration, PackageLockEntry, PackageSource, PackageStore,
};
use tempfile::tempdir;

fn write_package(root: &std::path::Path, name: &str) {
    fs::create_dir_all(root).unwrap();
    fs::write(
        root.join("package.toml"),
        format!(
            "name = \"{name}\"\n\
             description = \"{name} package\"\n\
             version = \"0.1.0\"\n"
        ),
    )
    .unwrap();
}

fn write_package_with_opi_version(root: &std::path::Path, name: &str, opi_version: &str) {
    fs::create_dir_all(root).unwrap();
    fs::write(
        root.join("package.toml"),
        format!(
            "name = \"{name}\"\n\
             description = \"{name} package\"\n\
             version = \"0.1.0\"\n\
             opi_version = \"{opi_version}\"\n"
        ),
    )
    .unwrap();
}

#[test]
fn manifest_sha256_returns_lowercase_hex_sha256_digest() {
    let dir = tempdir().unwrap();
    let path = dir.path().join("input.txt");
    fs::write(&path, "hello world").unwrap();

    let hash = manifest_sha256(&path).unwrap();

    assert_eq!(
        hash,
        "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
    );
}

#[test]
fn source_identity_for_resolution_canonicalizes_local_paths_against_base() {
    let workspace = tempdir().unwrap();
    let package_root = workspace.path().join("vendor").join("todo");
    write_package(&package_root, "todo");
    let source = PackageSource::parse("./vendor/../vendor/todo").unwrap();

    let identity = source_identity_for_resolution(&source, workspace.path()).unwrap();

    assert_eq!(identity.kind, "local");
    assert_eq!(
        identity.value,
        package_root.canonicalize().unwrap().display().to_string()
    );
}

#[test]
fn resolver_reads_project_package_declaration_as_package_resource() {
    let workspace = tempdir().unwrap();
    let user = tempdir().unwrap();
    let package_root = workspace.path().join("vendor/todo");
    write_package(&package_root, "todo");

    let store = PackageStore::project(workspace.path().to_path_buf());
    store
        .write_declarations(&[PackageDeclaration {
            source: ".\\vendor\\todo".to_string(),
            filters: Default::default(),
        }])
        .unwrap();
    store
        .write_lock(&[PackageLockEntry {
            identity_kind: "local".to_string(),
            identity_value: package_root.canonicalize().unwrap().display().to_string(),
            source: ".\\vendor\\todo".to_string(),
            package_root: package_root.canonicalize().unwrap(),
            cache_path: None,
            git_commit: None,
            manifest_sha256: opi_coding_agent::package_resolver::manifest_sha256(
                &package_root.join("package.toml"),
            )
            .unwrap(),
        }])
        .unwrap();

    let result = resolve_installed_packages(workspace.path(), user.path()).unwrap();

    assert_eq!(result.diagnostics, []);
    assert_eq!(result.packages.len(), 1);
    assert_eq!(result.packages[0].scope, InstalledPackageScope::Project);
    assert_eq!(result.packages[0].package.manifest.name, "todo");
}

#[test]
fn resolver_reports_missing_local_package_as_error() {
    let workspace = tempdir().unwrap();
    let user = tempdir().unwrap();
    let store = PackageStore::project(workspace.path().to_path_buf());
    store
        .write_declarations(&[PackageDeclaration {
            source: "./missing".to_string(),
            filters: Default::default(),
        }])
        .unwrap();

    let result = resolve_installed_packages(workspace.path(), user.path()).unwrap();

    assert_eq!(result.packages.len(), 0);
    assert_eq!(result.diagnostics.len(), 1);
    assert_eq!(
        result.diagnostics[0].severity,
        PackageDiagnosticSeverity::Error
    );
    assert_eq!(result.diagnostics[0].code, "source_missing");
}

#[test]
fn resolver_rejects_non_local_lock_entry_for_local_package_source() {
    let workspace = tempdir().unwrap();
    let user = tempdir().unwrap();
    let package_root = workspace.path().join("vendor/todo");
    write_package(&package_root, "todo");

    let store = PackageStore::project(workspace.path().to_path_buf());
    store
        .write_declarations(&[PackageDeclaration {
            source: "./vendor/todo".to_string(),
            filters: Default::default(),
        }])
        .unwrap();
    store
        .write_lock(&[PackageLockEntry {
            identity_kind: "git".to_string(),
            identity_value: "https://example.com/todo.git".to_string(),
            source: "./vendor/todo".to_string(),
            package_root: package_root.canonicalize().unwrap(),
            cache_path: None,
            git_commit: Some("abc123".to_string()),
            manifest_sha256: manifest_sha256(&package_root.join("package.toml")).unwrap(),
        }])
        .unwrap();

    let result = resolve_installed_packages(workspace.path(), user.path()).unwrap();

    assert_eq!(result.packages.len(), 0);
    assert_eq!(result.diagnostics.len(), 1);
    assert_eq!(result.diagnostics[0].code, "lock_missing");
}

#[test]
fn resolver_rejects_git_lock_source_that_does_not_match_ref_pinned_declaration() {
    let workspace = tempdir().unwrap();
    let user = tempdir().unwrap();
    let package_root = user.path().join("cache").join("repo");
    write_package(&package_root, "gitpkg");

    let store = PackageStore::global(user.path().to_path_buf());
    store
        .write_declarations(&[PackageDeclaration {
            source: "git:https://example.com/repo.git@sha1".to_string(),
            filters: Default::default(),
        }])
        .unwrap();
    store
        .write_lock(&[PackageLockEntry {
            identity_kind: "git".to_string(),
            identity_value: "https://example.com/repo.git".to_string(),
            source: "git:https://example.com/repo.git@sha2".to_string(),
            package_root: package_root.canonicalize().unwrap(),
            cache_path: Some(package_root.canonicalize().unwrap()),
            git_commit: Some("sha2".to_string()),
            manifest_sha256: manifest_sha256(&package_root.join("package.toml")).unwrap(),
        }])
        .unwrap();

    let result = resolve_installed_packages(workspace.path(), user.path()).unwrap();

    assert_eq!(result.packages.len(), 0);
    assert_eq!(result.diagnostics.len(), 1);
    assert_eq!(result.diagnostics[0].code, "lock_source_mismatch");
}

#[test]
fn resolver_reports_missing_manifest_declared_resource_asset() {
    let workspace = tempdir().unwrap();
    let user = tempdir().unwrap();
    let package_root = workspace.path().join("vendor/todo");
    fs::create_dir_all(&package_root).unwrap();
    fs::write(
        package_root.join("package.toml"),
        "name = \"todo\"\n\
         description = \"todo package\"\n\
         version = \"0.1.0\"\n\
         skills = [\"review\"]\n",
    )
    .unwrap();

    let store = PackageStore::project(workspace.path().to_path_buf());
    store
        .write_declarations(&[PackageDeclaration {
            source: "./vendor/todo".to_string(),
            filters: Default::default(),
        }])
        .unwrap();
    store
        .write_lock(&[opi_coding_agent::package_resolver::local_lock_entry(
            "./vendor/todo".to_string(),
            &package_root,
        )
        .unwrap()])
        .unwrap();

    let result = resolve_installed_packages(workspace.path(), user.path()).unwrap();

    assert_eq!(result.packages.len(), 0);
    assert_eq!(result.diagnostics.len(), 1);
    assert_eq!(result.diagnostics[0].code, "composition_failed");
    assert!(
        result.diagnostics[0]
            .message
            .contains("missing skill 'review'"),
        "diagnostic should identify missing declared skill asset: {:?}",
        result.diagnostics[0]
    );
}

#[test]
fn resolver_reports_incompatible_opi_version_as_warning() {
    let workspace = tempdir().unwrap();
    let user = tempdir().unwrap();
    let package_root = workspace.path().join("vendor/versioned");
    write_package_with_opi_version(&package_root, "versioned", ">=99.0");

    let store = PackageStore::project(workspace.path().to_path_buf());
    store
        .write_declarations(&[PackageDeclaration {
            source: "./vendor/versioned".to_string(),
            filters: Default::default(),
        }])
        .unwrap();
    store
        .write_lock(&[opi_coding_agent::package_resolver::local_lock_entry(
            "./vendor/versioned".to_string(),
            &package_root,
        )
        .unwrap()])
        .unwrap();

    let result = resolve_installed_packages(workspace.path(), user.path()).unwrap();

    assert_eq!(result.packages.len(), 1);
    assert_eq!(result.diagnostics.len(), 1);
    assert_eq!(
        result.diagnostics[0].severity,
        PackageDiagnosticSeverity::Warning
    );
    assert_eq!(result.diagnostics[0].code, "opi_version_incompatible");
    assert!(
        result.diagnostics[0]
            .message
            .contains("incompatible opi version"),
        "diagnostic should identify opi_version incompatibility: {:?}",
        result.diagnostics[0]
    );
}

#[test]
fn resolver_prefers_project_package_over_global_package_with_same_manifest_name() {
    let workspace = tempdir().unwrap();
    let user = tempdir().unwrap();
    let global_root = user.path().join("global-todo");
    let project_root = workspace.path().join("project-todo");
    write_package(&global_root, "todo");
    write_package(&project_root, "todo");

    let global = PackageStore::global(user.path().to_path_buf());
    global
        .write_declarations(&[PackageDeclaration {
            source: global_root.display().to_string(),
            filters: Default::default(),
        }])
        .unwrap();
    global
        .write_lock(&[opi_coding_agent::package_resolver::local_lock_entry(
            global_root.display().to_string(),
            &global_root,
        )
        .unwrap()])
        .unwrap();

    let project = PackageStore::project(workspace.path().to_path_buf());
    project
        .write_declarations(&[PackageDeclaration {
            source: project_root.display().to_string(),
            filters: Default::default(),
        }])
        .unwrap();
    project
        .write_lock(&[opi_coding_agent::package_resolver::local_lock_entry(
            project_root.display().to_string(),
            &project_root,
        )
        .unwrap()])
        .unwrap();

    let result = resolve_installed_packages(workspace.path(), user.path()).unwrap();

    assert_eq!(result.packages.len(), 1);
    assert_eq!(result.packages[0].scope, InstalledPackageScope::Project);
    assert_eq!(
        result.packages[0].package.path,
        project_root.canonicalize().unwrap()
    );
}
