use koto_runtime::KValue;
use koto_test_utils::*;

mod iterator {
    use super::*;

    mod next {
        use super::*;

        #[test]
        fn null_in_output() {
            let script = "
x = (1, null, 'x').iter()
x.next().get() # 1
x.next().get() # null
";
            check_script_output(script, KValue::Null);
        }
    }

    mod next_back {
        use super::*;

        #[test]
        fn null_in_output() {
            let script = "
x = (1, null, 'x').iter()
x.next_back().get() # 'x'
x.next_back().get() # null
";
            check_script_output(script, KValue::Null);
        }
    }

    mod chain {
        use super::*;

        #[test]
        fn make_copy_in_first_iter() {
            let script = "
x = (10..12).chain 12..15
x.next().get() # 10
y = copy x
x.next().get() # 11
x.next().get() # 12
y.next().get()
";
            check_script_output(script, 11);
        }

        #[test]
        fn make_copy_in_second_iter() {
            let script = "
x = (0..2).chain 2..5
x.next().get() # 0
x.next().get() # 1
x.next().get() # 2
y = copy x
x.next().get() # 3
x.next().get() # 4
y.next().get()
";
            check_script_output(script, 3);
        }
    }

    mod chunks {
        use super::*;

        #[test]
        fn with_generator() {
            let script = "
generator = || 
  for i in 1..=4
    yield i
generator()
  .chunks 2
  .skip 1
  .flatten()
  .to_tuple()
";
            check_script_output(script, number_tuple(&[3, 4]));
        }

        #[test]
        fn with_peekable() {
            let script = "
(1..=5)
  .peekable()
  .chunks 2
  .each |w| w.to_tuple()
  .to_tuple()
";
            check_script_output(
                script,
                tuple(&[
                    number_tuple(&[1, 2]),
                    number_tuple(&[3, 4]),
                    number_tuple(&[5]),
                ]),
            );
        }
    }

    mod cycle {
        use super::*;

        #[test]
        fn empty_input() {
            let script = "
[].cycle().next()
";
            check_script_output(script, KValue::Null);
        }

        #[test]
        fn make_copy() {
            let script = "
x = (1..=3).cycle()
x.next().get() # 1
y = copy x
x.next().get() # 2
x.next().get() # 3
y.next().get()
";
            check_script_output(script, 2);
        }

        #[test]
        fn with_peekable() {
            let script = "
(1..=3)
  .peekable()
  .cycle()
  .take 7
  .to_tuple()
";
            check_script_output(script, number_tuple(&[1, 2, 3, 1, 2, 3, 1]));
        }
    }

    mod each {
        use super::*;

        #[test]
        fn make_copy() {
            let script = "
x = (3, 4, 5, 6).each |x| x * x
x.next().get() # 9
y = copy x
x.next().get() # 16
x.next().get() # 25
y.next().get()
";
            check_script_output(script, 16);
        }

        #[test]
        fn each_reversed() {
            let script = "
x = (2, 4, 6)
 .each |x| x * x
 .reversed()
x.next().get()
";
            check_script_output(script, 36);
        }

        #[test]
        fn each_with_native_function() {
            let script = "
(2.5, 4.6, 6.1)
 .each number.round
 .to_tuple()
";
            check_script_output(script, number_tuple(&[3, 5, 6]));
        }
    }

    mod enumerate {
        use super::*;

        #[test]
        fn as_standalone_function() {
            let script = "
x = iterator.enumerate 'abc'
x.next().get()
";
            check_script_output(script, tuple(&[0.into(), "a".into()]));
        }

        #[test]
        fn make_copy() {
            let script = "
x = (10..20).enumerate()
x.next().get() # 0, 10
y = copy x
x.next().get() # 1, 11
x.next().get() # 2, 12
y.next().get()
";
            check_script_output(script, tuple(&[1.into(), 11.into()]));
        }
    }

    mod intersperse {
        use super::*;

        #[test]
        fn intersperse_by_value_make_copy() {
            let script = "
x = (1, 2, 3).intersperse 0
x.next().get() # 1
x.next().get() # 0
y = copy x
x.next().get() # 2
x.next().get() # 0
y.next().get()
";
            check_script_output(script, 2);
        }

        #[test]
        fn intersperse_with_function_make_copy() {
            let script = "
x = (10, 20, 30).intersperse || 42
x.next().get() # 10
x.next().get() # 42
y = copy x
x.next().get() # 20
x.next().get() # 42
y.next().get()
";
            check_script_output(script, 20);
        }
    }

    mod generate {
        use super::*;

        #[test]
        fn with_inline_function() {
            let script = "
add = |a, b| a + b
iterator.generate(|| add(2, 3), 5).to_tuple()
";
            check_script_output(script, number_tuple(&[5, 5, 5, 5, 5]));
        }
    }

    mod keep {
        use super::*;

        #[test]
        fn make_copy() {
            let script = "
x = 'abcdef'.chars().keep |c| 'bef'.contains c
x.next().get() # 'b'
y = copy x
x.next().get() # 'e'
y.next().get()
";
            check_script_output(script, "e");
        }
    }

    mod peekable {
        use super::*;
        use KValue::Null;

        #[test]
        fn peek() {
            let script = "
i = (1, 2, 3).peekable()
result = []
result.push i.peek().get() # 1
result.push i.next().get() # 1
result.push i.next().get() # 2
result.push i.peek().get() # 3
result.push i.next().get() # 3
result.push i.peek() # null
result.push i.next() # null
result
";
            check_script_output(
                script,
                list(&[1.into(), 1.into(), 2.into(), 3.into(), 3.into(), Null, Null]),
            );
        }

        #[test]
        fn peek_back_forwards() {
            let script = "
i = (1, 2, 3).peekable()
result = []
result.push i.peek().get() # 1
result.push i.peek_back().get() # 3
result.push i.peek_back().get() # 3
result.push i.next().get() # 1
result.push i.next().get() # 2
result.push i.peek().get() # 3
result.push i.next().get() # 3
result.push i.next() # null
result.push i.next_back() # null
result.push i.peek_back() # null
result
";
            check_script_output(
                script,
                list(&[
                    1.into(),
                    3.into(),
                    3.into(),
                    1.into(),
                    2.into(),
                    3.into(),
                    3.into(),
                    Null,
                    Null,
                    Null,
                ]),
            );
        }

        #[test]
        fn peek_back_backwards() {
            let script = "
i = (1, 2, 3).peekable()
result = []
result.push i.peek().get() # 1
result.push i.peek_back().get() # 3
result.push i.peek_back().get() # 3
result.push i.next_back().get() # 3
result.push i.next_back().get() # 2
result.push i.peek_back().get() # 1
result.push i.next_back().get() # 1
result.push i.peek_back() # null
result.push i.next_back() # null
result.push i.next() # null
result
";
            check_script_output(
                script,
                list(&[
                    1.into(),
                    3.into(),
                    3.into(),
                    3.into(),
                    2.into(),
                    1.into(),
                    1.into(),
                    Null,
                    Null,
                    Null,
                ]),
            );
        }
    }

    mod repeat {
        use super::*;

        #[test]
        fn repeat_used_as_imported_function() {
            //  Call iterator.repeat several times.
            //  iterator.repeat checks the instance register (aka 'frame base') to ensure that it's
            //  being used as a standalone function. The runtime now clears the frame base for
            //  non-instance functions, but previously this test would fail due to a temporary value
            //  being left around in the frame base.
            let script = "
from iterator import repeat
repeat(1, 4).to_tuple()
repeat(2, 3).to_tuple()
repeat(3, 2).to_tuple()
";
            check_script_output(script, number_tuple(&[3, 3]));
        }
    }

    mod skip {
        use super::*;

        #[test]
        fn skip_past_end_then_collect_shouldnt_panic() {
            let script = "
[].skip(1).to_tuple()
";
            check_script_output(script, tuple(&[]));
        }
    }

    mod take {
        use super::*;

        #[test]
        fn make_copy() {
            let script = "
x = 'abcdef'.chars().take 4
x.next().get() # 'a'
x.next().get() # 'b'
y = copy x
x.next().get() # 'c'
y.next().get()
";
            check_script_output(script, "c");
        }
    }

    mod windows {
        use super::*;

        #[test]
        fn with_a_generator() {
            let script = "
generator = ||
  for i in 1..=4
    yield i
generator()
  .windows 2
  .flatten()
  .to_tuple()
";
            check_script_output(script, number_tuple(&[1, 2, 2, 3, 3, 4]));
        }

        #[test]
        fn in_for_loop() {
            let script = "
result = []
for a, b in (1..=5).windows(2)
  result.push a
  result.push b
result
";
            check_script_output(script, number_list(&[1, 2, 2, 3, 3, 4, 4, 5]));
        }

        #[test]
        fn with_peekable() {
            let script = "
(1..=5)
  .peekable()
  .windows 3
  .each |w| w.to_tuple()
  .to_tuple()
";
            check_script_output(
                script,
                tuple(&[
                    number_tuple(&[1, 2, 3]),
                    number_tuple(&[2, 3, 4]),
                    number_tuple(&[3, 4, 5]),
                ]),
            );
        }
    }

    mod zip {
        use super::*;

        #[test]
        fn make_copy() {
            let script = "
x = (1..5).zip 11..15
x.next().get() # (1, 11)
x.next().get() # (2, 12)
y = copy x
x.next().get() # (3, 13)
y.next().get()
";
            check_script_output(script, number_tuple(&[3, 13]));
        }

        #[test]
        fn for_loop_over_many_zipped_values() {
            // This ensures that unpacking temporary value pairs in a for loop
            // doesn't overflow the register stack.
            let script = "
r = 1..=128
for a, b in r.zip r
  () # no-op
b
";
            check_script_output(script, 128);
        }
    }
}

mod map {
    use super::*;

    mod keys {
        use super::*;

        #[test]
        fn make_copy() {
            let script = "
x = {foo: 42, bar: 99, baz: -1}.keys()
x.next().get() # foo
y = copy x
x.next().get() # bar
y.next().get()
";
            check_script_output(script, "bar");
        }
    }

    mod values {
        use super::*;

        #[test]
        fn make_copy() {
            let script = "
x = {foo: 42, bar: 99, baz: -1}.values()
x.next().get() # 42
y = copy x
x.next().get() # 99
y.next().get()
";
            check_script_output(script, 99);
        }
    }
}

mod string {
    use super::*;

    mod graphemes {
        use super::*;

        #[test]
        fn next_back() {
            let script = "
'abc'.next_back().get()
";
            check_script_output(script, "c");
        }
    }

    mod bytes {
        use super::*;

        #[test]
        fn make_copy() {
            let script = "
x = 'abc'.bytes()
x.next().get() # 97
y = copy x
x.next().get() # 98
y.next().get()
";
            check_script_output(script, 98);
        }
    }

    mod char_indices {
        use super::*;

        #[test]
        fn text() {
            let script = "
'xßîहिं'.char_indices().to_tuple()
";
            check_script_output(
                script,
                tuple(&[range(0..1), range(1..3), range(3..5), range(5..14)]),
            );
        }

        #[test]
        fn emojis() {
            let script = "
'👍🫶🏽🫱🏼‍🫲🏾'.char_indices().to_tuple()
";
            check_script_output(script, tuple(&[range(0..4), range(4..12), range(12..31)]));
        }
    }

    mod lines {
        use super::*;

        #[test]
        fn make_copy() {
            let script = "
x = 'abc\ndef\nxyz'.lines()
x.next().get() # abc
y = copy x
x.next().get() # def
y.next().get()
";
            check_script_output(script, "def");
        }

        #[test]
        fn crlf_line_endings() {
            let script = "
'abc\r\ndef\r\nxyz\r\n\r\n'.lines().to_tuple()
";
            check_script_output(
                script,
                tuple(&["abc".into(), "def".into(), "xyz".into(), "".into()]),
            );
        }
    }

    mod split {
        use super::*;

        #[test]
        fn make_copy_pattern() {
            let script = "
x = '1-2-3'.split '-'
x.next().get() # 1
y = copy x
x.next().get() # 2
y.next().get()
";
            check_script_output(script, "2");
        }

        #[test]
        fn make_copy_predicate() {
            let script = "
x = '1-2_3'.split |c| '-_'.contains c
x.next().get() # 1
y = copy x
x.next().get() # 2
y.next().get()
";
            check_script_output(script, "2");
        }
    }
}
