use crate::atom::{Atom, AtomTable};
use crate::native::ProcessContext;
use crate::native::stdlib_stubs::lists_bifs::{
    bif_lists_append_1, bif_lists_append_2, bif_lists_join, bif_lists_reverse_2, bif_lists_seq,
};
use crate::native::stdlib_stubs::maps_bifs::{
    bif_maps_find, bif_maps_keys, bif_maps_put, bif_maps_to_list, bif_maps_values, bif_maps_with,
    bif_maps_without,
};
use crate::process::Process;
use crate::term::Term;
use crate::term::boxed::{Cons, Map, Tuple, write_map};
use crate::term::compare;

fn context(process: &mut Process) -> ProcessContext<'_> {
    let mut context = ProcessContext::new();
    context.set_atom_table(Some(std::sync::Arc::new(AtomTable::with_common_atoms())));
    context.attach_process(process, 0);
    context
}

fn badarg() -> Term {
    Term::atom(Atom::BADARG)
}

fn list_from_slice(ctx: &mut ProcessContext, elements: &[Term]) -> Term {
    let mut tail = Term::NIL;
    for element in elements.iter().rev() {
        tail = ctx
            .alloc_cons(*element, tail)
            .expect("test cons allocation");
    }
    tail
}

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

fn map_from_pairs(pairs: &[(Term, Term)]) -> Term {
    let atom_table = AtomTable::with_common_atoms();
    let mut sorted = pairs.to_vec();
    sorted.sort_by(|(left, _), (right, _)| compare::cmp(*left, *right, &atom_table));
    let keys: Vec<_> = sorted.iter().map(|(key, _)| *key).collect();
    let values: Vec<_> = sorted.iter().map(|(_, value)| *value).collect();
    let heap: &mut [u64] = Box::leak(vec![0_u64; 2 + pairs.len() * 2].into_boxed_slice());
    write_map(heap, &keys, &values).expect("test map allocation")
}

fn assert_map_entries(term: Term, expected: &[(Term, Term)]) {
    let map = Map::new(term).expect("map result");
    assert_eq!(map.len(), expected.len());
    for (key, value) in expected {
        assert_eq!(map.get(*key), Some(*value));
    }
}

#[test]
fn maps_put_inserts_and_replaces_entries() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);
    let empty = map_from_pairs(&[]);
    let inserted = bif_maps_put(&[Term::atom(Atom::OK), Term::small_int(1), empty], &mut ctx)
        .expect("put insert");
    assert_map_entries(inserted, &[(Term::atom(Atom::OK), Term::small_int(1))]);

    let replaced = bif_maps_put(
        &[Term::atom(Atom::OK), Term::small_int(2), inserted],
        &mut ctx,
    )
    .expect("put replace");
    assert_map_entries(replaced, &[(Term::atom(Atom::OK), Term::small_int(2))]);
}

#[test]
fn maps_find_returns_ok_tuple_or_error_atom() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);
    let map = map_from_pairs(&[(Term::atom(Atom::OK), Term::small_int(7))]);
    let found = bif_maps_find(&[Term::atom(Atom::OK), map], &mut ctx).expect("find hit");
    let tuple = Tuple::new(found).expect("{ok, value}");
    assert_eq!(tuple.arity(), 2);
    assert_eq!(tuple.get(0), Some(Term::atom(Atom::OK)));
    assert_eq!(tuple.get(1), Some(Term::small_int(7)));
    assert_eq!(
        bif_maps_find(&[Term::atom(Atom::ERROR), map], &mut ctx),
        Ok(Term::atom(Atom::ERROR))
    );
}

#[test]
fn maps_keys_values_and_to_list_project_sorted_entries() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);
    let map = map_from_pairs(&[
        (Term::atom(Atom::ERROR), Term::small_int(2)),
        (Term::atom(Atom::OK), Term::small_int(1)),
    ]);

    assert_eq!(
        list_to_vec(bif_maps_keys(&[map], &mut ctx).expect("keys")),
        vec![Term::atom(Atom::ERROR), Term::atom(Atom::OK)]
    );
    assert_eq!(
        list_to_vec(bif_maps_values(&[map], &mut ctx).expect("values")),
        vec![Term::small_int(2), Term::small_int(1)]
    );

    let pairs = list_to_vec(bif_maps_to_list(&[map], &mut ctx).expect("to_list"));
    let first = Tuple::new(pairs[0]).expect("first pair");
    let second = Tuple::new(pairs[1]).expect("second pair");
    assert_eq!(first.get(0), Some(Term::atom(Atom::ERROR)));
    assert_eq!(first.get(1), Some(Term::small_int(2)));
    assert_eq!(second.get(0), Some(Term::atom(Atom::OK)));
    assert_eq!(second.get(1), Some(Term::small_int(1)));
}

#[test]
fn maps_with_and_without_select_entries_and_validate_keys_list() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);
    let map = map_from_pairs(&[
        (Term::atom(Atom::OK), Term::small_int(1)),
        (Term::atom(Atom::ERROR), Term::small_int(2)),
    ]);
    let keys = list_from_slice(&mut ctx, &[Term::atom(Atom::ERROR)]);

    let only = bif_maps_with(&[keys, map], &mut ctx).expect("with");
    assert_map_entries(only, &[(Term::atom(Atom::ERROR), Term::small_int(2))]);
    let without = bif_maps_without(&[keys, map], &mut ctx).expect("without");
    assert_map_entries(without, &[(Term::atom(Atom::OK), Term::small_int(1))]);
    assert_eq!(
        bif_maps_with(&[Term::small_int(1), map], &mut ctx),
        Err(badarg())
    );
}

#[test]
fn lists_append_flattens_list_of_lists_and_rejects_improper_parts() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);
    let left = list_from_slice(&mut ctx, &[Term::small_int(1), Term::small_int(2)]);
    let right = list_from_slice(&mut ctx, &[Term::small_int(3), Term::small_int(4)]);
    let lists = list_from_slice(&mut ctx, &[left, right]);

    let result = bif_lists_append_1(&[lists], &mut ctx).expect("append/1");
    assert_eq!(
        list_to_vec(result),
        vec![
            Term::small_int(1),
            Term::small_int(2),
            Term::small_int(3),
            Term::small_int(4)
        ]
    );
    assert_eq!(bif_lists_append_1(&[left], &mut ctx), Err(badarg()));
}

#[test]
fn lists_append_two_preserves_right_tail_and_validates_left_list() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);
    let left = list_from_slice(&mut ctx, &[Term::small_int(1), Term::small_int(2)]);
    let right = list_from_slice(&mut ctx, &[Term::small_int(3), Term::small_int(4)]);

    let result = bif_lists_append_2(&[left, right], &mut ctx).expect("append/2");
    assert_eq!(
        list_to_vec(result),
        vec![
            Term::small_int(1),
            Term::small_int(2),
            Term::small_int(3),
            Term::small_int(4)
        ]
    );
    assert_eq!(
        bif_lists_append_2(&[Term::small_int(1), right], &mut ctx),
        Err(badarg())
    );
}

#[test]
fn lists_join_inserts_separator_and_handles_empty_list() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);
    let list = list_from_slice(
        &mut ctx,
        &[Term::small_int(1), Term::small_int(2), Term::small_int(3)],
    );

    let result = bif_lists_join(&[Term::atom(Atom::OK), list], &mut ctx).expect("join");
    assert_eq!(
        list_to_vec(result),
        vec![
            Term::small_int(1),
            Term::atom(Atom::OK),
            Term::small_int(2),
            Term::atom(Atom::OK),
            Term::small_int(3)
        ]
    );
    assert_eq!(
        bif_lists_join(&[Term::atom(Atom::OK), Term::NIL], &mut ctx),
        Ok(Term::NIL)
    );
}

#[test]
fn lists_reverse_two_prepends_reversed_list_to_tail() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);
    let list = list_from_slice(
        &mut ctx,
        &[Term::small_int(1), Term::small_int(2), Term::small_int(3)],
    );
    let tail = list_from_slice(&mut ctx, &[Term::small_int(4), Term::small_int(5)]);

    let result = bif_lists_reverse_2(&[list, tail], &mut ctx).expect("reverse/2");
    assert_eq!(
        list_to_vec(result),
        vec![
            Term::small_int(3),
            Term::small_int(2),
            Term::small_int(1),
            Term::small_int(4),
            Term::small_int(5)
        ]
    );
    assert_eq!(
        bif_lists_reverse_2(&[Term::small_int(1), tail], &mut ctx),
        Err(badarg())
    );
}

#[test]
fn lists_seq_builds_inclusive_range_and_empty_descending_range() {
    let mut process = Process::new(1, 256);
    let mut ctx = context(&mut process);
    let result =
        bif_lists_seq(&[Term::small_int(1), Term::small_int(5)], &mut ctx).expect("seq ascending");
    assert_eq!(
        list_to_vec(result),
        vec![
            Term::small_int(1),
            Term::small_int(2),
            Term::small_int(3),
            Term::small_int(4),
            Term::small_int(5)
        ]
    );
    assert_eq!(
        bif_lists_seq(&[Term::small_int(5), Term::small_int(1)], &mut ctx),
        Ok(Term::NIL)
    );
}
