package picard.sam.util;

import htsjdk.samtools.util.CollectionUtil;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.io.*;
import java.util.List;

/**
 * Tests for the ReadNameParser class.
 */
public class ReadNameParserTests {
    /** Tests rapidParseInt for positive and negative numbers, as well as non-digit suffixes */
    @Test
    public void testRapidParseInt() {
        for (int i = -100; i < 100; i++) {
            Assert.assertEquals(ReadNameParser.rapidParseInt(Integer.toString(i)), i);

            // trailing characters
            Assert.assertEquals(ReadNameParser.rapidParseInt(Integer.toString(i)+"A"), i);
            Assert.assertEquals(ReadNameParser.rapidParseInt(Integer.toString(i)+"ACGT"), i);
            Assert.assertEquals(ReadNameParser.rapidParseInt(Integer.toString(i)+".1"), i);
        }
    }

    /** Tests rapidParseInt for positive and negative numbers, as well as non-digit suffixes */
    @Test
    public void testRapidParseIntFails() {
        List<String> values = CollectionUtil.makeList("foo", "bar", "abc123", "-foo", "f00", "-f00");
        for (String s : values) {
            try {
                ReadNameParser.rapidParseInt(s);
                Assert.fail("Should have failed to rapid-parse " + s + " as an int.");
            }
            catch (NumberFormatException nfe) {
                /* expected */
            }
        }
    }

    /** Helper for testGetRapidDefaultReadNameRegexSplit */
    private void doTestGetRapidDefaultReadNameRegexSplit(int numFields) {
        final int[] inputFields = new int[3];
        final int[] expectedFields = new int[3];
        String readName = "";
        for (int i = 0; i < numFields; i++) {
            if (0 < i) readName += ":";
            readName += Integer.toString(i);
        }
        inputFields[0] = inputFields[1] = inputFields[2] = -1;
        if (numFields < 3) {
            Assert.assertEquals(ReadNameParser.getLastThreeFields(readName, ':', inputFields), -1);
        }
        else {
            Assert.assertEquals(ReadNameParser.getLastThreeFields(readName, ':', inputFields), numFields);
            expectedFields[0] = expectedFields[1] = expectedFields[2] = -1;
            if (0 < numFields) expectedFields[0] = numFields-3;
            if (1 < numFields) expectedFields[1] = numFields-2;
            if (2 < numFields) expectedFields[2] = numFields-1;
            for (int i = 0; i < inputFields.length; i++) {
                Assert.assertEquals(inputFields[i], expectedFields[i]);
            }
        }
    }

    /** Tests that we split the string with the correct # of fields, and modified values */
    @Test
    public void testGetRapidDefaultReadNameRegexSplit() {
        for (int i = 1; i < 10; i++) {
            doTestGetRapidDefaultReadNameRegexSplit(i);
        }
    }

    @DataProvider(name = "testParseReadNameDataProvider")
    public Object[][] testParseReadNameDataProvider() {
        return new Object[][]{
                {"RUNID:7:1203:2886:82292", 1203, 2886, 82292},
                {"RUNID:7:1203:2884:16834", 1203, 2884, 16834}
        };
    }

    // NB: these test fail s due to overflow in the duplicate finder test.  This has been the behavior previously, so keep it for now.
    @Test(dataProvider = "testParseReadNameDataProvider", enabled = true)
    public void testParseReadNameOverflow(final String readName, final int tile, final int x, final int y) {
        ReadNameParser parser = new ReadNameParser();
        PhysicalLocation loc = new PhysicalLocationShort();
        Assert.assertTrue(parser.addLocationInformation(readName, loc));
        Assert.assertEquals(loc.getTile(), tile);
        Assert.assertEquals(loc.getX(), (short)x); // casting to short for the overflow
        Assert.assertEquals(loc.getY(), (short)y); // casting to short for the overflow
    }

    // NB: this test the case where we do not overflow in the duplicate finder test.
    @Test(dataProvider = "testParseReadNameDataProvider", enabled = true)
    public void testParseReadNameOK(final String readName, final int tile, final int x, final int y) {
        ReadNameParser parser = new ReadNameParser();
        PhysicalLocation loc = new PhysicalLocationInt();
        Assert.assertTrue(parser.addLocationInformation(readName, loc));
        Assert.assertEquals(loc.getTile(), tile);
        Assert.assertEquals(loc.getX(), x); // we store ints, so we should not overflow
        Assert.assertEquals(loc.getY(), y); // we store ints, so we should not overflow
    }

    @DataProvider(name = "testReadNameParsing")
    public Object[][] testReadNameParsingDataProvider() {
        final String lastThreeFieldsRegex = "(?:.*:)?([0-9]+)[^:]*:([0-9]+)[^:]*:([0-9]+)[^:]*$";
        return new Object[][]{
                {lastThreeFieldsRegex, "RUNID:123:000000000-ZZZZZ:1:1105:17981:23325", 1105, 17981, 23325, true},
                {lastThreeFieldsRegex, "RUNID:123:000000000-ZZZZZ:1:1109:22981:17995", 1109, 22981, 17995, true},
                {lastThreeFieldsRegex, "1109:22981:17995", 1109, 22981, 17995, true},
                {lastThreeFieldsRegex, "RUNID:7:1203:2886:82292", 1203, 2886, 82292, true},
                {lastThreeFieldsRegex, "RUNID:7:1203:2884:16834", 1203, 2884, 16834, true},
                {lastThreeFieldsRegex, "1109ABC:22981DEF:17995GHI", 1109, 22981, 17995, true},
                {ReadNameParser.DEFAULT_READ_NAME_REGEX, "RUNID:123:000000000-ZZZZZ:1:1105:17981:23325", 1105, 17981, 23325, true},
                {ReadNameParser.DEFAULT_READ_NAME_REGEX, "RUNID:123:000000000-ZZZZZ:1:1109:22981:17995", 1109, 22981, 17995, true},
                {ReadNameParser.DEFAULT_READ_NAME_REGEX, "1109:22981:17995", 1109, 22981, 17995, false},
                {ReadNameParser.DEFAULT_READ_NAME_REGEX, "RUNID:7:1203:2886:82292", 1203, 2886, 82292, true},
                {ReadNameParser.DEFAULT_READ_NAME_REGEX, "RUNID:7:1203:2884:16834", 1203, 2884, 16834, true}
        };
    }

    @Test(dataProvider = "testReadNameParsing")
    public void testReadNameParsing(final String readNameRegex, final String readName, final int tile, final int x, final int y, final boolean addLocationInformationSucceeds) {
        final ReadNameParser parser = new ReadNameParser(readNameRegex);
        final PhysicalLocationInt loc = new PhysicalLocationInt();
        Assert.assertEquals(parser.addLocationInformation(readName, loc), addLocationInformationSucceeds);
        if (addLocationInformationSucceeds) { // just check the location
            Assert.assertEquals(loc.getTile(), tile);
            Assert.assertEquals(loc.getX(), x);
            Assert.assertEquals(loc.getY(), y);
        }
        else if (readNameRegex == ReadNameParser.DEFAULT_READ_NAME_REGEX) { // additional testing on the default regex
            int[] tokens = new int[3];
            ReadNameParser.getLastThreeFields(readName, ':', tokens);
            Assert.assertEquals(tokens[0], tile);
            Assert.assertEquals(tokens[1], x);
            Assert.assertEquals(tokens[2], y);
        }
    }

    @Test
    // Testing that the parser behavior stays constant after being java serialized
    public void testSerializedReadNameParser () throws IOException, ClassNotFoundException {
        // A non standard parser
        final String lastThreeFieldsRegex = "(?:.*:)?([0-9]+)[^:]*:([0-9]+)[^:]*:([0-9]+)[^:]*$";
        final ReadNameParser parser = new ReadNameParser(lastThreeFieldsRegex);
        final ReadNameParser defaultParser = new ReadNameParser();
        PhysicalLocationInt loc = new PhysicalLocationInt();
        final String readName = "1109ABC:22981DEF:17995GHI";
        final int expectedTile = 1109;
        final int expectedX = 22981;
        final int expectedY = 17995;

        Assert.assertTrue(parser.addLocationInformation(readName, loc));
        Assert.assertEquals(loc.getTile(), expectedTile);
        Assert.assertEquals(loc.getX(), expectedX);
        Assert.assertEquals(loc.getY(), expectedY);

        loc = new PhysicalLocationInt();
        Assert.assertFalse(defaultParser.addLocationInformation(readName, loc));

        final byte[] serializedBytes;
        try(final ByteArrayOutputStream baos = new ByteArrayOutputStream();
            final ObjectOutputStream out = new ObjectOutputStream(baos)) {
            out.writeObject(parser);
            out.writeObject(defaultParser);
            serializedBytes = baos.toByteArray();
        }
        try(final ByteArrayInputStream bais = new ByteArrayInputStream(serializedBytes);
            final ObjectInputStream in = new ObjectInputStream(bais)) {
            final ReadNameParser parserSerialized = (ReadNameParser) in.readObject();
            final ReadNameParser defaultParserSerialized = (ReadNameParser) in.readObject();

            loc = new PhysicalLocationInt();
            Assert.assertTrue(parserSerialized.addLocationInformation(readName, loc));
            Assert.assertEquals(loc.getTile(), expectedTile);
            Assert.assertEquals(loc.getX(), expectedX);
            Assert.assertEquals(loc.getY(), expectedY);

            loc = new PhysicalLocationInt();
            Assert.assertFalse(defaultParserSerialized.addLocationInformation(readName, loc));
        }
    }
}
