//! Unit tests for OTP stub BIFs.

use super::*;
use crate::atom::AtomTable;
use crate::native::BifRegistryImpl;
use crate::native::code_management_bifs::CodeManagementFacility;
use crate::process::Process;
use crate::term::binary_ref::BinaryRef;
use crate::term::boxed::{Cons, Tuple};
use crate::{
    error::LoadError,
    module::{ModuleOrigin, PurgeError},
    scheduler::{HotLoadResult, PurgeResult},
};
use std::collections::HashMap;
use std::sync::Arc;

#[test]
fn application_stopped_returns_ok() {
    let mut context = ProcessContext::new();
    let result = bif_application_stopped(&[], &mut context);
    assert_eq!(result, Ok(Term::atom(Atom::OK)));
}

#[test]
fn application_stopped_rejects_args() {
    let mut context = ProcessContext::new();
    let result = bif_application_stopped(&[Term::atom(Atom::OK)], &mut context);
    assert!(result.is_err());
}

#[test]
fn supervisor_start_link_rejects_wrong_arity() {
    let mut context = ProcessContext::new();
    let result = bif_supervisor_start_link(&[], &mut context);
    assert!(result.is_err());
}

#[test]
fn register_otp_stubs_registers_all_entries() {
    let atom_table = AtomTable::with_common_atoms();
    init_otp_atoms(&atom_table);
    let registry = BifRegistryImpl::new();

    register_otp_stubs(&registry, &atom_table).expect("otp stub registration");

    let gleam_otp_ext = atom_table.intern("gleam_otp_external");
    let app_stopped = atom_table.intern("application_stopped");
    assert!(
        registry.lookup(gleam_otp_ext, app_stopped, 0).is_some(),
        "gleam_otp_external:application_stopped/0 should be registered"
    );

    let supervisor = atom_table.intern("supervisor");
    let start_link = atom_table.intern("start_link");
    assert!(
        registry.lookup(supervisor, start_link, 2).is_some(),
        "supervisor:start_link/2 should be registered"
    );

    let gleam_string = atom_table.intern("gleam@string");
    let inspect = atom_table.intern("inspect");
    assert!(
        registry.lookup(gleam_string, inspect, 1).is_none(),
        "gleam@* modules ship as bytecode and must not be shadowed by stubs"
    );

    let os = atom_table.intern("os");
    let getenv = atom_table.intern("getenv");
    assert!(
        registry.lookup(os, getenv, 0).is_some(),
        "os:getenv/0 should be registered"
    );
}

#[test]
fn register_otp_stubs_rejects_duplicate_registration() {
    let atom_table = AtomTable::with_common_atoms();
    init_otp_atoms(&atom_table);
    let registry = BifRegistryImpl::new();

    register_otp_stubs(&registry, &atom_table).expect("first");
    assert!(register_otp_stubs(&registry, &atom_table).is_err());
}

#[test]
fn os_getenv_0_returns_non_empty_environment_list() {
    let mut process = Process::new(1, 262_144);
    let mut context = ProcessContext::new();
    context.attach_process(&mut process, 0);

    let result = erlang_stubs::bif_os_getenv_0(&[], &mut context).expect("environment list");
    let variables = list_terms(result);
    assert!(!variables.is_empty());
    assert!(
        variables
            .into_iter()
            .all(|variable| BinaryRef::new(variable).is_some())
    );
}

#[test]
fn os_getenv_returns_binary_values_and_false_for_missing() {
    let mut process = Process::new(1, 4096);
    let mut context = ProcessContext::new();
    context.attach_process(&mut process, 0);

    let key = context.alloc_binary(b"PATH").expect("key binary");
    let path = erlang_stubs::bif_os_getenv_1(&[key], &mut context).expect("PATH lookup");
    let bytes = BinaryRef::new(path).expect("PATH value should be binary");
    assert!(!bytes.is_empty());

    let missing = context
        .alloc_binary(b"BEAMR_TEST_NONEXISTENT_VAR")
        .expect("missing key binary");
    assert_eq!(
        erlang_stubs::bif_os_getenv_1(&[missing], &mut context),
        Ok(Term::atom(Atom::FALSE))
    );
    assert!(erlang_stubs::bif_os_getenv_1(&[Term::small_int(1)], &mut context).is_err());
}

#[test]
fn os_putenv_and_unsetenv_round_trip() {
    let mut process = Process::new(1, 4096);
    let mut context = ProcessContext::new();
    context.attach_process(&mut process, 0);
    let key_name = b"BEAMR_TEST_B170_PUTENV";
    let key = context.alloc_binary(key_name).expect("key binary");
    let value = context.alloc_binary(b"hello").expect("value binary");
    let previous = std::env::var("BEAMR_TEST_B170_PUTENV").ok();

    assert_eq!(
        erlang_stubs::bif_os_putenv(&[key, value], &mut context),
        Ok(Term::atom(Atom::TRUE))
    );
    let lookup_key = context.alloc_binary(key_name).expect("lookup key binary");
    let lookup = erlang_stubs::bif_os_getenv_1(&[lookup_key], &mut context).expect("lookup");
    assert_eq!(binary_bytes(lookup), b"hello");

    let unset_key = context.alloc_binary(key_name).expect("unset key binary");
    assert_eq!(
        erlang_stubs::bif_os_unsetenv(&[unset_key], &mut context),
        Ok(Term::atom(Atom::TRUE))
    );
    let missing_key = context.alloc_binary(key_name).expect("missing key binary");
    assert_eq!(
        erlang_stubs::bif_os_getenv_1(&[missing_key], &mut context),
        Ok(Term::atom(Atom::FALSE))
    );
    assert!(erlang_stubs::bif_os_putenv(&[Term::small_int(1), value], &mut context).is_err());

    if let Some(previous) = previous {
        let restore_key = context.alloc_binary(key_name).expect("restore key");
        let restore_value = context
            .alloc_binary(previous.as_bytes())
            .expect("restore value");
        assert_eq!(
            erlang_stubs::bif_os_putenv(&[restore_key, restore_value], &mut context),
            Ok(Term::atom(Atom::TRUE))
        );
    }
}

#[test]
fn os_type_returns_platform_tuple() {
    let atom_table = Arc::new(AtomTable::with_common_atoms());
    init_otp_atoms(&atom_table);
    let mut process = Process::new(1, 128);
    let mut context = ProcessContext::new();
    context.set_atom_table(Some(atom_table.clone()));
    context.attach_process(&mut process, 0);

    let result = erlang_stubs::bif_os_type(&[], &mut context).expect("os:type");
    let tuple = Tuple::new(result).expect("os:type tuple");
    assert_eq!(tuple.arity(), 2);
    if cfg!(target_os = "macos") {
        assert_eq!(
            tuple.get(0).and_then(Term::as_atom),
            Some(atom_table.intern("unix"))
        );
        assert_eq!(
            tuple.get(1).and_then(Term::as_atom),
            Some(atom_table.intern("darwin"))
        );
    } else if cfg!(target_os = "linux") {
        assert_eq!(
            tuple.get(0).and_then(Term::as_atom),
            Some(atom_table.intern("unix"))
        );
        assert_eq!(
            tuple.get(1).and_then(Term::as_atom),
            Some(atom_table.intern("linux"))
        );
    } else if cfg!(target_os = "windows") {
        assert_eq!(
            tuple.get(0).and_then(Term::as_atom),
            Some(atom_table.intern("win32"))
        );
        assert_eq!(
            tuple.get(1).and_then(Term::as_atom),
            Some(atom_table.intern("nt"))
        );
    }
}

#[test]
fn code_priv_dir_resolves_sibling_priv_for_loaded_filesystem_module() {
    let atom_table = Arc::new(AtomTable::with_common_atoms());
    init_otp_atoms(&atom_table);
    let app = atom_table.intern("beamr_b170_app");
    let app_dir = unique_test_dir("beamr_b170_priv_dir");
    let ebin_dir = app_dir.join("ebin");
    let priv_dir = app_dir.join("priv");
    std::fs::create_dir_all(&ebin_dir).expect("create ebin");
    std::fs::create_dir_all(&priv_dir).expect("create priv");
    let beam_path = ebin_dir.join("beamr_b170_app.beam");
    std::fs::write(&beam_path, []).expect("create beam placeholder");

    let mut process = Process::new(1, 4096);
    let mut context = ProcessContext::new();
    context.set_atom_table(Some(atom_table));
    context.set_code_management_facility(Some(Arc::new(MockCodeFacility::with_origin(
        app,
        ModuleOrigin::Filesystem(beam_path),
    ))));
    context.attach_process(&mut process, 0);

    let result =
        erlang_stubs::bif_code_priv_dir(&[Term::atom(app)], &mut context).expect("code:priv_dir");
    assert_eq!(binary_bytes(result), priv_dir.to_string_lossy().as_bytes());
    assert!(erlang_stubs::bif_code_priv_dir(&[Term::small_int(1)], &mut context).is_err());

    std::fs::remove_dir_all(app_dir).expect("remove temp app dir");
}

#[test]
fn code_priv_dir_returns_error_bad_name_for_missing_app() {
    let atom_table = Arc::new(AtomTable::with_common_atoms());
    init_otp_atoms(&atom_table);
    let missing = atom_table.intern("missing_app");
    let mut process = Process::new(1, 1024);
    let mut context = ProcessContext::new();
    context.set_atom_table(Some(atom_table.clone()));
    context.set_code_management_facility(Some(Arc::new(MockCodeFacility::default())));
    context.attach_process(&mut process, 0);

    let result = erlang_stubs::bif_code_priv_dir(&[Term::atom(missing)], &mut context)
        .expect("bad_name tuple");
    let tuple = Tuple::new(result).expect("error tuple");
    assert_eq!(tuple.get(0), Some(Term::atom(Atom::ERROR)));
    assert_eq!(
        tuple.get(1).and_then(Term::as_atom),
        Some(atom_table.intern("bad_name"))
    );
}

#[test]
fn string_split_preserves_binary_and_list_representations() {
    let mut process = Process::new(1, 4096);
    let mut context = ProcessContext::new();
    context.attach_process(&mut process, 0);

    let input = context.alloc_binary(b"a.b.c").expect("input");
    let pattern = context.alloc_binary(b".").expect("pattern");
    let result = erlang_stubs::bif_string_split(&[input, pattern], &mut context).expect("split");
    let parts = list_terms(result);
    assert_eq!(parts.len(), 2);
    assert_eq!(binary_bytes(parts[0]), b"a");
    assert_eq!(binary_bytes(parts[1]), b"b.c");

    let no_match_input = context.alloc_binary(b"hello").expect("no match input");
    let no_match_pattern = context.alloc_binary(b".").expect("no match pattern");
    let no_match =
        erlang_stubs::bif_string_split(&[no_match_input, no_match_pattern], &mut context)
            .expect("no match split");
    assert_eq!(list_terms(no_match), vec![no_match_input]);

    let empty = context.alloc_binary(b"").expect("empty input");
    let dot = context.alloc_binary(b".").expect("dot");
    let empty_result =
        erlang_stubs::bif_string_split(&[empty, dot], &mut context).expect("empty no match");
    assert_eq!(list_terms(empty_result), vec![empty]);

    let list_input = byte_list(&mut context, b"a.b");
    let list_pattern = byte_list(&mut context, b".");
    let list_result = erlang_stubs::bif_string_split(&[list_input, list_pattern], &mut context)
        .expect("list split");
    let list_parts = list_terms(list_result);
    assert_eq!(list_bytes(list_parts[0]), b"a");
    assert_eq!(list_bytes(list_parts[1]), b"b");
    assert!(erlang_stubs::bif_string_split(&[Term::small_int(1), pattern], &mut context).is_err());
}

#[test]
fn ensure_all_started_reports_loaded_and_missing_modules() {
    let atom_table = Arc::new(AtomTable::with_common_atoms());
    init_otp_atoms(&atom_table);
    let loaded = atom_table.intern("loaded_app");
    let missing = atom_table.intern("missing_app");
    let mut process = Process::new(1, 4096);
    let mut context = ProcessContext::new();
    context.set_atom_table(Some(atom_table.clone()));
    context.set_code_management_facility(Some(Arc::new(MockCodeFacility::with_origin(
        loaded,
        ModuleOrigin::Preloaded,
    ))));
    context.attach_process(&mut process, 0);

    let ok = erlang_stubs::bif_ensure_all_started(&[Term::atom(loaded)], &mut context)
        .expect("loaded result");
    let ok_tuple = Tuple::new(ok).expect("ok tuple");
    assert_eq!(ok_tuple.get(0), Some(Term::atom(Atom::OK)));
    assert_eq!(
        list_terms(ok_tuple.get(1).expect("started app list")),
        vec![Term::atom(loaded)]
    );

    let error = erlang_stubs::bif_ensure_all_started(&[Term::atom(missing)], &mut context)
        .expect("missing result");
    let error_tuple = Tuple::new(error).expect("error tuple");
    assert_eq!(error_tuple.get(0), Some(Term::atom(Atom::ERROR)));
    let reason = Tuple::new(error_tuple.get(1).expect("reason tuple")).expect("reason tuple");
    assert_eq!(reason.get(0), Some(Term::atom(missing)));
    let inner = Tuple::new(reason.get(1).expect("inner tuple")).expect("inner tuple");
    assert_eq!(
        inner.get(0).and_then(Term::as_atom),
        Some(atom_table.intern("not_loaded"))
    );
    assert_eq!(inner.get(1), Some(Term::atom(missing)));
    assert!(erlang_stubs::bif_ensure_all_started(&[Term::small_int(1)], &mut context).is_err());
}

#[test]
fn erlang_not_negates_booleans() {
    use crate::native::gate3_bifs::bif_not;

    let mut context = ProcessContext::new();
    assert_eq!(
        bif_not(&[Term::atom(Atom::TRUE)], &mut context),
        Ok(Term::atom(Atom::FALSE))
    );
    assert_eq!(
        bif_not(&[Term::atom(Atom::FALSE)], &mut context),
        Ok(Term::atom(Atom::TRUE))
    );
    assert!(bif_not(&[Term::atom(Atom::OK)], &mut context).is_err());
}

#[test]
fn erlang_length_counts_empty_list() {
    use crate::native::gate3_bifs::bif_length;

    let mut context = ProcessContext::new();
    assert_eq!(
        bif_length(&[Term::NIL], &mut context),
        Ok(Term::small_int(0))
    );
}

#[derive(Default)]
struct MockCodeFacility {
    origins: HashMap<Atom, ModuleOrigin>,
}

impl MockCodeFacility {
    fn with_origin(module: Atom, origin: ModuleOrigin) -> Self {
        Self {
            origins: HashMap::from([(module, origin)]),
        }
    }
}

impl CodeManagementFacility for MockCodeFacility {
    fn load_module(&self, _bytes: &[u8]) -> Result<HotLoadResult, LoadError> {
        unimplemented!("not needed by OTP stub tests")
    }

    fn purge_module(&self, _module: Atom) -> Result<PurgeResult, PurgeError> {
        unimplemented!("not needed by OTP stub tests")
    }

    fn delete_module(&self, _module: Atom) -> bool {
        unimplemented!("not needed by OTP stub tests")
    }

    fn check_old_code(&self, _module: Atom) -> bool {
        false
    }

    fn check_process_code(&self, _pid: u64, _module: Atom) -> bool {
        false
    }

    fn module_origin(&self, module: Atom) -> Option<ModuleOrigin> {
        self.origins.get(&module).cloned()
    }

    fn all_loaded_modules(&self) -> Vec<(Atom, ModuleOrigin)> {
        self.origins
            .iter()
            .map(|(module, origin)| (*module, origin.clone()))
            .collect()
    }
}

fn binary_bytes(term: Term) -> &'static [u8] {
    BinaryRef::new(term).expect("binary term").as_bytes()
}

fn byte_list(context: &mut ProcessContext, bytes: &[u8]) -> Term {
    let elements: Vec<_> = bytes
        .iter()
        .map(|byte| Term::small_int(i64::from(*byte)))
        .collect();
    context.alloc_list(&elements).expect("byte list")
}

fn list_terms(term: Term) -> Vec<Term> {
    let mut terms = Vec::new();
    let mut current = term;
    while !current.is_nil() {
        let cons = Cons::new(current).expect("proper list");
        terms.push(cons.head());
        current = cons.tail();
    }
    terms
}

fn list_bytes(term: Term) -> Vec<u8> {
    list_terms(term)
        .into_iter()
        .map(|term| u8::try_from(term.as_small_int().expect("byte integer")).expect("u8 byte"))
        .collect()
}

fn unique_test_dir(prefix: &str) -> std::path::PathBuf {
    std::env::temp_dir().join(format!(
        "{prefix}_{}_{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("system clock after epoch")
            .as_nanos()
    ))
}
