package com.atlassian.bitbucket.scm.cache;

import org.apache.commons.io.IOUtils;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedList;

public abstract class TestSupport {

    public static class MockProcess {
        private final LinkedList<StreamData> data = new LinkedList<>();

        public MockProcess output(InputStream stream) {
            data.add(new StreamData(StreamType.OUTPUT, stream));
            return this;
        }

        public OutputStream getStdIn() {
            return new OutputStream() {
                @Override
                public void write(int b) throws IOException {
                    synchronized (data) {
                        waitUntil(StreamType.INPUT);
                        if (data.isEmpty()) {
                            throw new IllegalStateException("No data expected");
                        }
                        if (!data.getFirst().matches(b)) {
                            throw new IllegalStateException("Unexpected data");
                        }
                        popIfDepleted();
                    }
                }
            };
        }

        public InputStream getStdOut() {
            return new InputStream() {
                @Override
                public int read() throws IOException {
                    synchronized (data) {
                        waitUntil(StreamType.OUTPUT);
                        if (data.isEmpty()) {
                            return -1;
                        }
                        try {
                            return data.getFirst().read();
                        } finally {
                            popIfDepleted();
                        }
                    }
                }
            };
        }

        public InputStream getStdErr() {
            return new InputStream() {
                @Override
                public int read() throws IOException {
                    synchronized (data) {
                        waitUntil(StreamType.ERROR);
                        if (data.isEmpty()) {
                            return -1;
                        }
                        try {
                            return data.getFirst().read();
                        } finally {
                            popIfDepleted();
                        }
                    }
                }
            };
        }

        private void popIfDepleted() throws IOException {
            synchronized (data) {
                if (data.getFirst().isDepleted()) {
                    data.getFirst().close();
                    data.pop();
                    data.notifyAll();
                }
            }
        }

        private void waitUntil(StreamType streamType) {
            while (!data.isEmpty() && data.getFirst().getType() != streamType) {
                try {
                    data.wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new IllegalStateException("interruped!");
                }
            }
        }
    }


    private static class StreamData {
        private final StreamType type;
        private final BufferedInputStream stream;

        private StreamData(StreamType type, InputStream stream) {
            this.type = type;
            this.stream = new BufferedInputStream(stream, 32);
        }

        public void close() {
            IOUtils.closeQuietly(stream);
        }

        public StreamType getType() {
            return type;
        }

        public boolean isDepleted() throws IOException {
            if (stream.available() > 0) {
                return false;
            }
            stream.mark(1);
            try {
                return -1 == stream.read();
            } finally {
                stream.reset();
            }
        }

        public boolean matches(int b) throws IOException {
            return stream.read() == b;
        }

        public int read() throws IOException {
            return stream.read();
        }
    }

    private enum StreamType {

        INPUT,
        OUTPUT,
        ERROR
    }
}
