/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package accord.verify;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertThrows;

public class StrictSerializabilityVerifierTest
{
    private static final int[][] SEQUENCES = new int[][] { null, new int[]{ }, new int[] { 1 }, new int[] { 1, 2 }, new int[] { 1, 2, 3 } };

    static class History
    {
        final int keys;
        int overlap, nextAt;
        List<Observation> observations = new ArrayList<>();

        History(int keys)
        {
            this.keys = keys;
        }

        History w(int writeKey, int writeSeq)
        {
            return rw(writeKey, writeSeq, (int[])null);
        }

        History ws(int ... writes)
        {
            return rw(writes, (int[])null);
        }

        History rw(int writeKey, int writeSeq, int ... reads)
        {
            int[] writes;
            if (writeKey < 0)
            {
                writes = new int[0];
            }
            else
            {
                writes = new int[writeKey + 1];
                Arrays.fill(writes, -1);
                writes[writeKey] = writeSeq;
            }
            return rw(writes, reads);
        }

        History rw(int[] writes, int ... reads)
        {
            int start = overlap > 0 ? this.overlap : ++nextAt;
            int end = ++nextAt;
            observations.add(new Observation(writes, reads, start, end));
            return this;
        }

        History overlap()
        {
            overlap = ++nextAt;
            return this;
        }

        History deoverlap()
        {
            overlap = 0;
            return this;
        }

        History r(int ... reads)
        {
            return rw(-1, -1, reads);
        }

        void assertNoViolation()
        {
            run(Runnable::run);
        }

        void assertViolation()
        {
            run(runnable -> assertThrows(HistoryViolation.class, runnable::run));
        }

        private void run(Consumer<Runnable> onLast)
        {
            StrictSerializabilityVerifier verifier = new StrictSerializabilityVerifier("", keys);
            for (int i = 0 ; i < observations.size() ; ++i)
            {
                Observation observation = observations.get(i);
                verifier.begin();
                if (observation.reads != null)
                {
                    for (int key = 0 ; key < observation.reads.length ; ++key)
                    {
                        if (observation.reads[key] >= 0)
                            verifier.witnessRead(key, SEQUENCES[observation.reads[key] + 1]);
                    }
                }
                for (int j = 0 ; j < observation.writes.length ; ++j)
                {
                    if (observation.writes[j] >= 0)
                        verifier.witnessWrite(j, observation.writes[j]);
                }

                if (i == observations.size() - 1) onLast.accept(() -> verifier.apply("", observation.start, observation.end));
                else verifier.apply("", observation.start, observation.end);
            }
        }
    }

    static class Observation
    {
        final int[] writes;
        final int[] reads;
        final int start, end;

        Observation(int[] writes, int[] reads, int start, int end)
        {
            this.writes = writes;
            this.reads = reads;
            this.start = start;
            this.end = end;
        }
    }

    @Test
    public void lostRead()
    {
        new History(1).r(1).r(0).assertViolation();
        new History(1).r(2).r(1).assertViolation();
        new History(2).r(1, 2).r(2, 1).assertViolation();
    }

    @Test
    public void lostWrite()
    {
        new History(1).w(0, 1).r(0).assertViolation();
        new History(1).rw(0, 1, 0).r(0).assertViolation();
    }

    @Test
    public void readBeforeWrite()
    {
        new History(1).r(1).rw(0, 1, 0).assertViolation();
        new History(1).r(1).w(0, 1).assertViolation();
    }

    @Test
    public void readCycle()
    {
        new History(2).overlap().r(0, 1).r(1, 0).assertViolation();
        new History(3).overlap().r(1, 0, -1).r(-1, 1, 0).r(0, -1, 1).assertViolation();
        new History(4).overlap().r(1, 0, -1, -1).r(-1, 1, 0, -1).r(-1, -1, 1, 0).r(0, -1, -1, 1).assertViolation();
    }

    @Test
    public void readWriteCycle()
    {
        new History(2).overlap().rw(0, 1, -1, 0).rw(1, 1, 1, -1).assertNoViolation();
        new History(2).overlap().rw(0, 1, -1, 1).rw(1, 1, 0, -1).assertNoViolation();
        new History(2).overlap().rw(0, 1, -1, 0).rw(1, 1, 0, -1).assertViolation();
        new History(2).overlap().rw(0, 1, -1, 0).w(1, 1).r(0, 1).assertViolation();
    }

    @Test
    public void simpleWrite()
    {
        new History(1).r(0).w(0, 1).r(1).assertNoViolation();
        new History(1).rw(0, 1, 0).r(1).assertNoViolation();
    }

    @Test
    public void blindWrites()
    {
        new History(2).ws(1, 1).r(1, -1).ws(-1, 2).assertNoViolation();
        new History(2).ws(1, 1).ws(-1, 2).assertNoViolation();
        new History(2).ws(1, 1).ws(2, 2).r(2).assertNoViolation();
        new History(2).ws(1, 1).ws(2, 2).r(2).r(1).assertViolation();
        new History(2).ws(1, 1).overlap().ws(2, 2).r(2).r(1).deoverlap().r(2).assertNoViolation();
        new History(2).ws(1, 1).ws(2, 2).r(1).assertViolation();
    }

    private void fromLog(String log)
    {
        class Read
        {
            final int pk, id, count;
            final int[] seq;

            Read(int pk, int id, int count, int[] seq)
            {
                this.pk = pk;
                this.id = id;
                this.count = count;
                this.seq = seq;
            }
        }
        class Write
        {
            final int pk, id;
            final boolean success;

            Write(int pk, int id, boolean success)
            {
                this.pk = pk;
                this.id = id;
                this.success = success;
            }
        }
        class Witness
        {
            final int start, end;
            final List<Object> actions = new ArrayList<>();

            Witness(int start, int end)
            {
                this.start = start;
                this.end = end;
            }

            public void read(int pk, int id, int count, int[] seq)
            {
                actions.add(new Read(pk, id, count, seq));
            }

            public void write(int pk, int id, boolean success)
            {
                actions.add(new Write(pk, id, success));
            }

            public void process(StrictSerializabilityVerifier verifier)
            {
                verifier.begin();
                for (Object a : actions)
                {
                    if (a instanceof Read)
                    {
                        Read read = (Read) a;
                        verifier.witnessRead(read.pk, read.seq);
                    }
                    else
                    {
                        Write write = (Write) a;
                        if (write.success)
                            verifier.witnessWrite(write.pk, write.id);
                    }
                }
                verifier.apply("", start, end);
            }
        }
        List<Witness> witnesses = new ArrayList<>();
        Witness current = null;
        for (String line : log.split("\n"))
        {
            if (line.startsWith("Witness"))
            {
                if (current != null)
                    witnesses.add(current);
                Matcher matcher = Pattern.compile("Witness\\(start=(.+), end=(.+)\\)").matcher(line);
                if (!matcher.find()) throw new AssertionError("Unable to match start/end of " + line);
                current = new Witness(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)));
            }
            else if (line.startsWith("\tread"))
            {
                Matcher matcher = Pattern.compile("\tread\\(pk=(.+), id=(.+), count=(.+), seq=\\[(.*)\\]\\)").matcher(line);
                if (!matcher.find()) throw new AssertionError("Unable to match read of " + line);
                int pk = Integer.parseInt(matcher.group(1));
                int id = Integer.parseInt(matcher.group(2));
                int count = Integer.parseInt(matcher.group(3));
                String seqStr = matcher.group(4);
                int[] seq = seqStr.isEmpty() ? new int[0] : Stream.of(seqStr.split(",")).map(String::trim).mapToInt(Integer::parseInt).toArray();
                current.read(pk, id, count, seq);
            }
            else if (line.startsWith("\twrite"))
            {
                Matcher matcher = Pattern.compile("\twrite\\(pk=(.+), id=(.+), success=(.+)\\)").matcher(line);
                if (!matcher.find()) throw new AssertionError("Unable to match write of " + line);
                int pk = Integer.parseInt(matcher.group(1));
                int id = Integer.parseInt(matcher.group(2));
                boolean success = Boolean.parseBoolean(matcher.group(3));
                current.write(pk, id, success);
            }
            else
            {
                throw new IllegalArgumentException("Unknown line: " + line);
            }
        }

        if (current != null)
            witnesses.add(current);

        StrictSerializabilityVerifier validator = new StrictSerializabilityVerifier("", 3);
        for (Witness w : witnesses)
            w.process(validator);
    }

    @Test
    public void seenBehavior()
    {
        fromLog("Witness(start=4, end=7)\n" +
                "\tread(pk=0, id=2, count=0, seq=[])\n" +
                "\twrite(pk=0, id=2, success=true)\n" +
                "Witness(start=3, end=8)\n" +
                "\tread(pk=2, id=0, count=0, seq=[])\n" +
                "\twrite(pk=2, id=0, success=true)\n" +
                "\twrite(pk=1, id=0, success=true)\n" +
                "Witness(start=5, end=9)\n" +
                "\tread(pk=0, id=3, count=1, seq=[2])\n" +
                "\twrite(pk=0, id=3, success=true)\n" +
                "Witness(start=2, end=10)\n" +
                "\twrite(pk=2, id=1, success=true)\n" +
                "\twrite(pk=1, id=1, success=true)\n" +
                "Witness(start=6, end=11)\n" +
                "\tread(pk=0, id=4, count=2, seq=[2, 3])\n" +
                "\twrite(pk=0, id=4, success=true)\n" +
                "Witness(start=12, end=14)\n" +
                "\twrite(pk=0, id=5, success=true)\n" +
                "Witness(start=13, end=16)\n" +
                "\tread(pk=1, id=6, count=2, seq=[0, 1])\n" +
                "\twrite(pk=1, id=6, success=true)\n" +
                "\twrite(pk=2, id=6, success=true)\n" +
                "Witness(start=15, end=18)\n" +
                "\tread(pk=0, id=7, count=4, seq=[2, 3, 4, 5])\n" +
                "\twrite(pk=0, id=7, success=true)\n" +
                "Witness(start=17, end=20)\n" +
                "\tread(pk=1, id=8, count=3, seq=[0, 1, 6])\n" +
                "\twrite(pk=1, id=8, success=true)\n" +
                "\twrite(pk=2, id=8, success=true)\n");
    }
}
