package queue;

import base.Asserts;
import base.ExtendedRandom;
import base.TestCounter;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
 */
public class ArrayQueueTester<M extends Queues.QueueModel> {
    private static final int OPERATIONS = 50_000;
    /* package-private */ static final Object[] ELEMENTS = new Object[]{
            "Hello",
            "world",
            1, 2, 3,
            List.of("a"),
            List.of("a"),
            List.of("b"),
            Map.of()
    };

    private final Class<M> model;
    private final Queues.QueueChecker<M> tester;

    protected ArrayQueueTester(final Class<M> model, final Queues.QueueChecker<M> tester, final Queues.Splitter<M> splitter) {
        this.model = model;
        this.tester = new Queues.QueueChecker<>() {
            @Override
            public M wrap(final ArrayDeque<Object> reference) {
                return tester.wrap(reference);
            }

            @Override
            public void check(final M queue, final ExtendedRandom random) {
                tester.check(queue, random);
            }

            @Override
            public void add(final M queue, final Object element, final ExtendedRandom random) {
                tester.add(queue, element, random);
            }

            @Override
            public void remove(final M queue, final ExtendedRandom random) {
                tester.remove(queue, random);
            }

            @Override
            public List<M> linearTest(final M queue, final ExtendedRandom random) {
                if (random.nextInt(50) == 0) {
                    queue.clear();
                    return List.of();
                } else {
                    return splitter.split(this, queue, random);
                }
            }
        };
    }

    protected void test(final TestCounter counter) {
        test(counter, "ArrayQueue", ReflectionTest.Mode.values());
    }

    protected void test(final TestCounter counter, final String className, final ReflectionTest.Mode... modes) {
        for (final ReflectionTest.Mode mode : modes) {
            final String scope = "Running %s for %s in %s mode%n"
                    .formatted(model.getEnclosingClass().getSimpleName(), className, mode);
            counter.scope(scope, () -> new Variant(counter).test(className, mode));
        }
    }



    protected void checkImplementation(final Class<?> implementation) {
        // Do nothing by default
    }

    private static List<Object> toList(final Queues.QueueModel queue) {
        final List<Object> list = Stream.generate(queue::dequeue).limit(queue.size()).toList();
        list.forEach(queue::enqueue);
        return list;
    }

    protected static ArrayDeque<Object> collect(final Stream<Object> elements) {
        return elements.collect(Collectors.toCollection(ArrayDeque::new));
    }

    private class Variant extends ReflectionTest {
        private final TestCounter counter;

        public Variant(final TestCounter counter) {
            this.counter = counter;
        }

        protected void testEmpty(final M queue) {
            counter.scope("testEmpty", () -> assertSize(0, queue));
        }

        protected void testSingleton(final M queue) {
            counter.scope("testSingleton", () -> {
                assertSize(0, queue);
                final String value = "value";
                queue.enqueue(value);
                assertSize(1, queue);
                Asserts.assertEquals("element()", value, queue.element());
                Asserts.assertEquals("dequeue()", value, queue.dequeue());
                assertSize(0, queue);
            });
        }

        protected void testClear(final M queue) {
            counter.scope("testClear", () -> {
                assertSize(0, queue);

                final String value = "value";
                queue.enqueue(value);
                queue.enqueue(value);
                queue.clear();
                assertSize(0, queue);

                final String value1 = "value1";
                queue.enqueue(value1);
                Asserts.assertEquals("deque()", value1, queue.dequeue());
            });
        }

        private int checkAndSize(final M queue) {
            final int size = queue.size();
            if (!queue.isEmpty() && random().nextBoolean()) {
                tester.check(queue, random());
            }
            return size;
        }

        protected Object randomElement() {
            return ELEMENTS[random().nextInt(ELEMENTS.length)];
        }

        protected void assertSize(final int size, final M queue) {
            counter.test(() -> {
                Asserts.assertEquals("size()", size, queue.size());
                Asserts.assertTrue("Expected isEmpty() " + (size == 0) + ", found " + queue.isEmpty(), (size == 0) == queue.isEmpty());
            });
        }

        @Override
        protected void checkResult(final String call, final Object expected, final Object actual) {
            if (expected instanceof Queues.QueueModel model) {
                super.checkResult(call, toList(model), toList((Queues.QueueModel) actual));
            } else {
                super.checkResult(call, expected, actual);
            }
        }

        protected Supplier<M> factory(final String name, final Mode mode) {
            final ProxyFactory<M> factory = new ProxyFactory<>(model, mode, "queue." + name);
            checkImplementation(factory.implementation);
            return () -> checking(counter, model, tester.wrap(new ArrayDeque<>()), factory.create());
        }

        private void test(final String className, final Mode mode) {
            final Supplier<M> factory = factory(className, mode);
            testEmpty(factory.get());
            testSingleton(factory.get());
            testClear(factory.get());
            for (int i = 0; i <= 10; i += 2) {
                testRandom(factory.get(), (double) i / 10);
            }
        }

        private void testRandom(final M initial, final double addFreq) {
            counter.scope("testRandom, add frequency = " + addFreq, () -> {
                final List<M> queues = new ArrayList<>();
                queues.add(initial);
                int ops = 0;
                for (int i = 0; i < OPERATIONS / TestCounter.DENOMINATOR / TestCounter.DENOMINATOR; i++) {
                    final M queue = queues.get(random().nextInt(queues.size()));

                    final int size = counter.testV(() -> {
                        if (queue.isEmpty() || random().nextDouble() < addFreq) {
                            tester.add(queue, randomElement(), random());
                        } else {
                            tester.remove(queue, random());
                        }

                        return checkAndSize(queue);
                    });

                    if (ops++ >= size && random().nextInt(4) == 0) {
                        ops -= size;

                        counter.test(() -> {
                            queues.addAll(tester.linearTest(queue, random()));
                            checkAndSize(queue);
                        });
                    }
                }

                for (final M queue : queues) {
                    counter.test(() -> {
                        tester.linearTest(queue, random());
                        checkAndSize(queue);
                        for (int i = queue.size(); i > 0; i--) {
                            tester.remove(queue, random());
                            checkAndSize(queue);
                        }
                    });
                }
            });
        }

        private ExtendedRandom random() {
            return counter.random();
        }
    }
}
