#![allow(clippy::unit_arg)]

use std::fmt::Debug;
use std::iter::FromIterator;
use std::panic::{catch_unwind, AssertUnwindSafe};

use proptest::{arbitrary::any, collection::vec, prelude::*, proptest};
use proptest_derive::Arbitrary;

use crate::ring_buffer::RingBuffer;

#[derive(Debug)]
struct InputVec<A>(Vec<A>);

impl<A> InputVec<A> {
    fn unwrap(self) -> Vec<A> {
        self.0
    }
}

impl<A> Arbitrary for InputVec<A>
where
    A: Arbitrary + Debug,
    <A as Arbitrary>::Strategy: 'static,
{
    type Parameters = usize;
    type Strategy = BoxedStrategy<InputVec<A>>;
    fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
        #[allow(clippy::redundant_closure)]
        proptest::collection::vec(any::<A>(), 0..RingBuffer::<u32>::CAPACITY)
            .prop_map(|v| InputVec(v))
            .boxed()
    }
}

#[derive(Arbitrary, Debug)]
enum Construct<A>
where
    A: Arbitrary,
    <A as Arbitrary>::Strategy: 'static,
{
    Empty,
    Single(A),
    Pair((A, A)),
    DrainFrom(InputVec<A>),
    CollectFrom(InputVec<A>, usize),
    FromFront(InputVec<A>, usize),
    FromBack(InputVec<A>, usize),
    FromIter(InputVec<A>),
}

#[derive(Arbitrary, Debug)]
enum Action<A>
where
    A: Arbitrary,
    <A as Arbitrary>::Strategy: 'static,
{
    PushFront(A),
    PushBack(A),
    PopFront,
    PopBack,
    DropLeft(usize),
    DropRight(usize),
    SplitOff(usize),
    Append(Construct<A>),
    DrainFromFront(Construct<A>, usize),
    DrainFromBack(Construct<A>, usize),
    Set(usize, A),
    Insert(usize, A),
    Remove(usize),
    Drain,
    Clear,
}

impl<A> Construct<A>
where
    A: Arbitrary + Clone + Debug + Eq,
    <A as Arbitrary>::Strategy: 'static,
{
    fn make(self) -> RingBuffer<A> {
        match self {
            Construct::Empty => {
                let out = RingBuffer::new();
                assert!(out.is_empty());
                out
            }
            Construct::Single(value) => {
                let out = RingBuffer::unit(value.clone());
                assert_eq!(out, vec![value]);
                out
            }
            Construct::Pair((left, right)) => {
                let out = RingBuffer::pair(left.clone(), right.clone());
                assert_eq!(out, vec![left, right]);
                out
            }
            Construct::DrainFrom(vec) => {
                let vec = vec.unwrap();
                let mut source = RingBuffer::from_iter(vec.iter().cloned());
                let out = RingBuffer::drain_from(&mut source);
                assert!(source.is_empty());
                assert_eq!(out, vec);
                out
            }
            Construct::CollectFrom(vec, len) => {
                let mut vec = vec.unwrap();
                if vec.is_empty() {
                    return RingBuffer::new();
                }
                let len = len % vec.len();
                let mut source = vec.clone().into_iter();
                let out = RingBuffer::collect_from(&mut source, len);
                let expected_remainder = vec.split_off(len);
                let remainder: Vec<_> = source.collect();
                assert_eq!(expected_remainder, remainder);
                assert_eq!(out, vec);
                out
            }
            Construct::FromFront(vec, len) => {
                let mut vec = vec.unwrap();
                if vec.is_empty() {
                    return RingBuffer::new();
                }
                let len = len % vec.len();
                let mut source = RingBuffer::from_iter(vec.iter().cloned());
                let out = RingBuffer::from_front(&mut source, len);
                let remainder = vec.split_off(len);
                assert_eq!(source, remainder);
                assert_eq!(out, vec);
                out
            }
            Construct::FromBack(vec, len) => {
                let mut vec = vec.unwrap();
                if vec.is_empty() {
                    return RingBuffer::new();
                }
                let len = len % vec.len();
                let mut source = RingBuffer::from_iter(vec.iter().cloned());
                let out = RingBuffer::from_back(&mut source, len);
                let remainder = vec.split_off(vec.len() - len);
                assert_eq!(out, remainder);
                assert_eq!(source, vec);
                out
            }
            Construct::FromIter(vec) => {
                let vec = vec.unwrap();
                let out = vec.clone().into_iter().collect();
                assert_eq!(out, vec);
                out
            }
        }
    }
}

proptest! {
    #[test]
    fn test_constructors(cons: Construct<u32>) {
        cons.make();
    }

    #[test]
    fn test_actions(cons: Construct<u32>, actions in vec(any::<Action<u32>>(), 0..super::action_count())) {
        let capacity = RingBuffer::<u32>::CAPACITY;
        let mut chunk = cons.make();
        let mut guide: Vec<_> = chunk.iter().cloned().collect();
        println!("{:?}", actions);
        for action in actions {
            println!("Executing {:?} on {:?}", action, chunk);
            match action {
                Action::PushFront(value) => {
                    if chunk.is_full() {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.push_front(value))).is_err());
                    } else {
                        chunk.push_front(value);
                        guide.insert(0, value);
                    }
                }
                Action::PushBack(value) => {
                    if chunk.is_full() {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.push_back(value))).is_err());
                    } else {
                        chunk.push_back(value);
                        guide.push(value);
                    }
                }
                Action::PopFront => {
                    assert_eq!(chunk.pop_front(), if guide.is_empty() {
                        None
                    } else {
                        Some(guide.remove(0))
                    });
                }
                Action::PopBack => {
                    assert_eq!(chunk.pop_back(), guide.pop());
                }
                Action::DropLeft(index) => {
                    if index >= chunk.len() {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.drop_left(index))).is_err());
                    } else {
                        chunk.drop_left(index);
                        guide.drain(..index);
                    }
                }
                Action::DropRight(index) => {
                    if index >= chunk.len() {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.drop_right(index))).is_err());
                    } else {
                        chunk.drop_right(index);
                        guide.drain(index..);
                    }
                }
                Action::SplitOff(index) => {
                    if index >= chunk.len() {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.split_off(index))).is_err());
                    } else {
                        let chunk_off = chunk.split_off(index);
                        let guide_off = guide.split_off(index);
                        assert_eq!(chunk_off, guide_off);
                    }
                }
                Action::Append(other) => {
                    let mut other = other.make();
                    let mut other_guide: Vec<_> = other.iter().cloned().collect();
                    if other.len() + chunk.len() > capacity {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.append(&mut other))).is_err());
                    } else {
                        chunk.append(&mut other);
                        guide.append(&mut other_guide);
                    }
                }
                Action::DrainFromFront(other, count) => {
                    let mut other = other.make();
                    let mut other_guide: Vec<_> = other.iter().cloned().collect();
                    if count >= other.len() || chunk.len() + count > capacity {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.drain_from_front(&mut other, count))).is_err());
                    } else {
                        chunk.drain_from_front(&mut other, count);
                        guide.extend(other_guide.drain(..count));
                        assert_eq!(other, other_guide);
                    }
                }
                Action::DrainFromBack(other, count) => {
                    let mut other = other.make();
                    let mut other_guide: Vec<_> = other.iter().cloned().collect();
                    if count >= other.len() || chunk.len() + count > capacity {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.drain_from_back(&mut other, count))).is_err());
                    } else {
                        chunk.drain_from_back(&mut other, count);
                        let other_index = other.len() - count;
                        guide = other_guide.drain(other_index..).chain(guide.into_iter()).collect();
                        assert_eq!(other, other_guide);
                    }
                }
                Action::Set(index, value) => {
                    if index >= chunk.len() {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.set(index, value))).is_err());
                    } else {
                        chunk.set(index, value);
                        guide[index] = value;
                    }
                }
                Action::Insert(index, value) => {
                    if index >= chunk.len() || chunk.is_full() {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.insert(index, value))).is_err());
                    } else {
                        chunk.insert(index, value);
                        guide.insert(index, value);
                    }
                }
                Action::Remove(index) => {
                    if index >= chunk.len() {
                        assert!(catch_unwind(AssertUnwindSafe(|| chunk.remove(index))).is_err());
                    } else {
                        assert_eq!(chunk.remove(index), guide.remove(index));
                    }
                }
                Action::Drain => {
                    let drained: Vec<_> = chunk.drain().collect();
                    let drained_guide: Vec<_> = guide.drain(..).collect();
                    assert_eq!(drained, drained_guide);
                }
                Action::Clear => {
                    chunk.clear();
                    guide.clear();
                }
            }
            assert_eq!(chunk, guide);
            assert!(guide.len() <= capacity);
        }
    }
}
