// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

mod common;

use std::process::Command;

use assert_cmd::{assert::OutputAssertExt, cargo::CommandCargoExt};
use rstest::rstest;

const DB_FLAG: &str = "--database=test_data/db/test_data_db";
const INVALID_DB_FLAG: &str = "--database=test_data/db/invalid_db";
const OLD_DB_FLAG: &str = "--database=test_data/db/old_db";

#[test]
fn test_locate_no_matches() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["usr", DB_FLAG])
        .assert()
        .failure();
}

#[test]
fn test_locate_match() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["test_data", DB_FLAG])
        .assert()
        .success();
}

#[test]
fn test_locate_no_matches_basename() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["test_data1234567890", "--basename", DB_FLAG])
        .assert()
        .failure();
}

#[test]
fn test_locate_match_basename() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["abbbc", "--basename", DB_FLAG])
        .assert()
        .success();
}

#[test]
fn test_locate_existing() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["abbbc", "--existing", DB_FLAG])
        .assert()
        .success();
}

#[test]
fn test_locate_non_existing() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["abbbc", "--non-existing", DB_FLAG])
        .assert()
        .failure();
}

#[test]
fn test_locate_statistics() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["", "--statistics", DB_FLAG])
        .assert()
        .success();
}

#[rstest]
#[case("emacs")]
#[case("grep")]
#[case("posix-basic")]
#[case("posix-extended")]
fn test_locate_regex(#[case] input: &str) {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args([
            "abbbc",
            "--regex",
            format!("--regextype={input}").as_str(),
            DB_FLAG,
        ])
        .assert()
        .success();
}

#[test]
fn test_locate_all() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["abb", "bbc", "--all", DB_FLAG])
        .assert()
        .success();
}

#[test]
fn test_locate_all_regex() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["abb", "b*c", "--all", "--regex", DB_FLAG])
        .assert()
        .success();
}

#[test]
fn test_locate_invalid_db() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["test_data", INVALID_DB_FLAG])
        .assert()
        .failure();
}

#[test]
fn test_locate_outdated_db() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["test_data", OLD_DB_FLAG])
        .assert()
        .success();
}

#[test]
fn test_locate_large_max_database_age() {
    for age in ["1000000000000", "18446744073709551615"] {
        Command::cargo_bin("locate")
            .expect("couldn't find locate binary")
            .arg("test_data")
            .arg(format!("--max-database-age={age}"))
            .arg(OLD_DB_FLAG)
            .assert()
            .success();
    }
}

#[test]
fn test_locate_print_help() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .arg("--help")
        .assert()
        .success();
}

#[test]
fn test_locate_invalid_flag() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .arg("--unknown")
        .assert()
        .failure();
}

// an un-compilable regex should be reported as an error rather than silently matching nothing
#[test]
fn test_locate_invalid_regex() {
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args(["[", "--regex", DB_FLAG])
        .assert()
        .failure();
}

#[test]
fn test_updatedb() {
    let tmp = tempfile::tempdir().unwrap();
    Command::cargo_bin("updatedb")
        .expect("couldn't find updatedb binary")
        .args([
            "--localpaths=./test_data".to_string(),
            format!("--output={}", tmp.path().join("db").display()),
        ])
        .assert()
        .success();
}

#[test]
fn test_updatedb_invalid_flag() {
    Command::cargo_bin("updatedb")
        .expect("couldn't find updatedb binary")
        .args(["--unknown"])
        .assert()
        .failure();
}

// empty prunefs/prunepaths must not produce an invalid find expression (e.g. an empty `( )` group)
#[test]
fn test_updatedb_empty_prune() {
    let tmp = tempfile::tempdir().unwrap();
    Command::cargo_bin("updatedb")
        .expect("couldn't find updatedb binary")
        .args([
            "--localpaths=./test_data".to_string(),
            format!("--output={}", tmp.path().join("db").display()),
            "--prunefs=".to_string(),
            "--prunepaths=".to_string(),
        ])
        .assert()
        .success();
}

// when the output database can't be created, updatedb must report a clear error naming the path
// and must not leak the raw "(os error N)" suffix
#[test]
fn test_updatedb_output_create_error() {
    let tmp = tempfile::tempdir().unwrap();
    // a path under a non-existent directory can't be created
    let bad_output = tmp.path().join("does-not-exist").join("db");
    let assert = Command::cargo_bin("updatedb")
        .expect("couldn't find updatedb binary")
        .args([
            "--localpaths=./test_data".to_string(),
            format!("--output={}", bad_output.display()),
        ])
        .assert()
        .failure();
    let stderr = String::from_utf8_lossy(&assert.get_output().stderr);
    assert!(
        stderr.contains("cannot create") && stderr.contains(&bad_output.display().to_string()),
        "stderr did not name the un-creatable output path: {stderr:?}"
    );
    assert!(
        !stderr.contains("os error"),
        "stderr leaked the raw OS error: {stderr:?}"
    );
}

// build a database from a temp tree with updatedb, then query it back with locate. This is the
// only test that exercises the full pipeline (writer + reader) and is platform-independent.
#[test]
fn test_updatedb_locate_roundtrip() {
    // create the tree under the target dir so the path has no spaces (updatedb splits
    // --localpaths on whitespace, mirroring GNU's space-separated convention)
    let dir = tempfile::tempdir_in(env!("CARGO_TARGET_TMPDIR")).unwrap();
    let marker = "locate_roundtrip_marker";
    std::fs::write(dir.path().join(marker), b"").unwrap();
    let db = dir.path().join("db");

    Command::cargo_bin("updatedb")
        .expect("couldn't find updatedb binary")
        .args([
            format!("--localpaths={}", dir.path().display()),
            format!("--output={}", db.display()),
        ])
        .assert()
        .success();

    let assert = Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .args([marker.to_string(), format!("--database={}", db.display())])
        .assert()
        .success();
    let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
    assert!(
        stdout.contains(marker),
        "locate output did not contain the indexed file: {stdout:?}"
    );
}

#[test]
fn test_locate_one_byte_db() {
    let dir = tempfile::tempdir().unwrap();
    let db = dir.path().join("db_1byte");
    std::fs::write(&db, b"X").unwrap();
    Command::cargo_bin("locate")
        .expect("couldn't find locate binary")
        .arg(format!("--database={}", db.display()))
        .arg("foo")
        .assert()
        .code(1);
}
