/*
 * The MIT License
 *
 * Copyright (c) 2009 The Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package htsjdk.samtools.util;

import htsjdk.HtsjdkTest;
import htsjdk.samtools.*;
import htsjdk.samtools.reference.ReferenceSequenceFile;
import htsjdk.samtools.reference.ReferenceSequenceFileFactory;
import java.io.File;
import java.util.*;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * @author alecw@broadinstitute.org
 */
public class SequenceUtilTest extends HtsjdkTest {
    private static final String HEADER = "@HD\tVN:1.0\tSO:unsorted\n";
    private static final String SEQUENCE_NAME =
            "@SQ\tSN:phix174.seq\tLN:5386\tUR:/seq/references/PhiX174/v0/PhiX174.fasta\tAS:PhiX174\tM5:3332ed720ac7eaa9b3655c06f6b9e196";

    @Test
    public void testExactMatch() {
        final SAMSequenceDictionary sd1 = makeSequenceDictionary(
                5386, "/seq/references/PhiX174/v0/PhiX174.fasta", "3332ed720ac7eaa9b3655c06f6b9e196");
        final SAMSequenceDictionary sd2 = makeSequenceDictionary(
                5386, "/seq/references/PhiX174/v0/PhiX174.fasta", "3332ed720ac7eaa9b3655c06f6b9e196");
        SequenceUtil.assertSequenceDictionariesEqual(sd1, sd2);
    }

    @DataProvider
    public Object[][] compatibleNonEqualLists() {
        final String s = HEADER
                + String.format(
                        "@SQ\tSN:phix174.seq\tLN:%d\tUR:%s\tAS:PhiX174\tM5:%s\n",
                        5386, "/seq/references/PhiX174/v0/PhiX174.fasta", "3332ed720ac7eaa9b3655c06f6b9e196")
                + String.format(
                        "@SQ\tSN:phix175.seq\tLN:%d\tUR:%s\tAS:HiMom\tM5:%s\n",
                        5385, "/seq/references/PhiX174/v0/HiMom.fasta", "deadbeed");

        return new Object[][] {
            {
                makeSequenceDictionary(
                        5386, "/seq/references/PhiX174/v0/PhiX174.fasta", "3332ed720ac7eaa9b3655c06f6b9e196"),
                new SAMTextHeaderCodec()
                        .decode(BufferedLineReader.fromString(s), null)
                        .getSequenceDictionary()
            }
        };
    }

    @Test(dataProvider = "compatibleNonEqualLists")
    public void testCompatible(SAMSequenceDictionary sd1, SAMSequenceDictionary sd2) {
        SequenceUtil.assertSequenceDictionariesEqual(sd1, sd2, true);
    }

    @Test(
            dataProvider = "compatibleNonEqualLists",
            expectedExceptions = SequenceUtil.SequenceListsDifferException.class)
    public void testinCompatible(SAMSequenceDictionary sd1, SAMSequenceDictionary sd2) {
        SequenceUtil.assertSequenceDictionariesEqual(sd1, sd2, false);
    }

    @Test(expectedExceptions = SequenceUtil.SequenceListsDifferException.class)
    public void testMismatch() {
        final SAMSequenceDictionary sd1 = makeSequenceDictionary(
                5386, "/seq/references/PhiX174/v0/PhiX174.fasta", "3332ed720ac7eaa9b3655c06f6b9e196");
        final SAMSequenceDictionary sd2 =
                makeSequenceDictionary(5386, "/seq/references/PhiX174/v0/PhiX174.fasta", "deadbeef");
        SequenceUtil.assertSequenceDictionariesEqual(sd1, sd2);
        Assert.fail();
    }

    @Test
    public void testFileColonDifference() {
        final SAMSequenceDictionary sd1 = makeSequenceDictionary(
                5386, "/seq/references/PhiX174/v0/PhiX174.fasta", "3332ed720ac7eaa9b3655c06f6b9e196");
        final SAMSequenceDictionary sd2 = makeSequenceDictionary(
                5386, "file:/seq/references/PhiX174/v0/PhiX174.fasta", "3332ed720ac7eaa9b3655c06f6b9e196");
        SequenceUtil.assertSequenceDictionariesEqual(sd1, sd2);
    }

    @Test
    public void testURDifferent() {
        final SAMSequenceDictionary sd1 = makeSequenceDictionary(
                5386, "/seq/references/PhiX174/v0/PhiX174.fasta", "3332ed720ac7eaa9b3655c06f6b9e196");
        final SAMSequenceDictionary sd2 = makeSequenceDictionary(
                5386, "file:/seq/references/PhiX174/v1/PhiX174.fasta", "3332ed720ac7eaa9b3655c06f6b9e196");
        SequenceUtil.assertSequenceDictionariesEqual(sd1, sd2);
    }

    private SAMSequenceDictionary makeSequenceDictionary(final int length, final String ur, final String m5) {
        final String s =
                HEADER + String.format("@SQ\tSN:phix174.seq\tLN:%d\tUR:%s\tAS:PhiX174\tM5:%s\n", length, ur, m5);
        return new SAMTextHeaderCodec()
                .decode(BufferedLineReader.fromString(s), null)
                .getSequenceDictionary();
    }

    @Test(dataProvider = "makeReferenceFromAlignment")
    public void testMakeReferenceFromAlignment(
            final String seq,
            final String cigar,
            final String md,
            boolean includeReferenceBasesForDeletions,
            final String expectedReference) {
        final SAMRecord rec = new SAMRecord(null);
        rec.setReadName("test");
        rec.setReadString(seq);
        rec.setCigarString(cigar);
        rec.setAttribute(SAMTag.MD, md);
        final byte[] refBases = SequenceUtil.makeReferenceFromAlignment(rec, includeReferenceBasesForDeletions);
        Assert.assertEquals(StringUtil.bytesToString(refBases), expectedReference);
    }

    @DataProvider(name = "makeReferenceFromAlignment")
    public Object[][] testMakeReferenceFromAlignmentDataProvider() {
        return new Object[][] {
            {"ACGTACGTACGT", "12M2H", "4GAAA4", true, "ACGTGAAAACGT"},
            {"ACGTACGTACGT", "2H12M", "12", false, "ACGTACGTACGT"},
            {"ACGTACGTACGT", "4M4I4M2H", "8", false, "ACGT----ACGT"},
            {"ACGTACGTACGT", "2S4M2I4M2S", "8", false, "00GTAC--ACGT00"},
            {"ACGTACGTACGT", "6M2D6M2H", "4GA^TT0TG4", true, "ACGTGATTTGACGT"},
            {"ACGTACGTACGT", "6M2D6M2H", "4GA^TT0TG4", false, "ACGTGATGACGT"},
            // When CIGAR has N, MD will not have skipped bases.
            {"ACGTACGTACGT", "6M2N6M2H", "4GA0TG4", true, "ACGTGANNTGACGT"},
            {"ACGTACGTACGT", "6M2N6M2H", "4GA0TG4", false, "ACGTGATGACGT"},
            {"ACGTACGTACGT", "6M2N6M2H", "4GATG4", true, "ACGTGANNTGACGT"},
            {"ACGTACGTACGT", "6M2N6M2H", "4GATG4", false, "ACGTGATGACGT"},
        };
    }

    @Test(dataProvider = "mismatchCountsDataProvider")
    public void testCountMismatches(
            final String readString,
            final String cigar,
            final String reference,
            final int expectedMismatchesExact,
            final int expectedMismatchesAmbiguous) {
        final SAMRecord rec = new SAMRecord(null);
        rec.setReadName("test");
        rec.setReadString(readString);
        final byte[] byteArray = new byte[readString.length()];

        Arrays.fill(byteArray, (byte) 33);

        rec.setBaseQualities(byteArray);
        rec.setCigarString(cigar);

        final byte[] refBases = StringUtil.stringToBytes(reference);

        final int nExact = SequenceUtil.countMismatches(rec, refBases, -1, false, false);
        Assert.assertEquals(nExact, expectedMismatchesExact);

        final int sumMismatchesQualityExact = SequenceUtil.sumQualitiesOfMismatches(rec, refBases, -1, false);
        Assert.assertEquals(sumMismatchesQualityExact, expectedMismatchesExact * 33);

        final int nAmbiguous = SequenceUtil.countMismatches(rec, refBases, -1, false, true);
        Assert.assertEquals(nAmbiguous, expectedMismatchesAmbiguous);
    }

    @DataProvider(name = "mismatchCountsDataProvider")
    public Object[][] testMakeMismatchCountsDataProvider() {
        // note: R=A|G
        return new Object[][] {
            {"A", "1M", "A", 0, 0},
            {"A", "1M", "R", 1, 0},
            {"G", "1M", "R", 1, 0},
            {"C", "1M", "R", 1, 1},
            {"T", "1M", "R", 1, 1},
            {"N", "1M", "R", 1, 1},
            {"R", "1M", "A", 1, 1},
            {"R", "1M", "C", 1, 1},
            {"R", "1M", "G", 1, 1},
            {"R", "1M", "T", 1, 1},
            {"R", "1M", "N", 1, 0},
            {"R", "1M", "R", 0, 0},
            {"N", "1M", "N", 0, 0}
        };
    }

    @DataProvider(name = "mismatchBisulfiteCountsDataProvider")
    public Object[][] mismatchBisulfiteCountsDataProvider() {

        List<Object[]> tests = new ArrayList<>();
        final List<String> bases = Arrays.asList("A", "C", "T", "G");

        for (final String base : bases) {
            for (final String ref : bases) {
                for (final Boolean strand : Arrays.asList(true, false)) {

                    final Integer count;

                    if (base.equals(ref)) count = 0;
                    else if (base.equals("A") && ref.equals("G") && !strand) count = 0;
                    else if (base.equals("T") && ref.equals("C") && strand) count = 0;
                    else count = 1;

                    tests.add(new Object[] {base, "1M", ref, strand, count});
                }
            }
        }
        return tests.toArray(new Object[1][]);
    }

    @Test(dataProvider = "mismatchBisulfiteCountsDataProvider")
    public void testMismatchBisulfiteCounts(
            final String readString,
            final String cigar,
            final String reference,
            final boolean positiveStrand,
            final int expectedMismatches) {

        final byte baseQuality = 30;
        final SAMRecord rec = new SAMRecord(null);
        rec.setReadName("test");
        rec.setReadString(readString);
        rec.setReadNegativeStrandFlag(!positiveStrand);
        final byte[] byteArray = new byte[readString.length()];

        Arrays.fill(byteArray, baseQuality);

        rec.setBaseQualities(byteArray);
        rec.setCigarString(cigar);

        final byte[] refBases = StringUtil.stringToBytes(reference);

        final int nExact = SequenceUtil.countMismatches(rec, refBases, -1, true, false);
        Assert.assertEquals(nExact, expectedMismatches);

        final int sumMismatchesQualityExact = SequenceUtil.sumQualitiesOfMismatches(rec, refBases, -1, true);
        Assert.assertEquals(sumMismatchesQualityExact, expectedMismatches * baseQuality);
    }

    @Test(dataProvider = "countInsertedAndDeletedBasesTestCases")
    public void testCountInsertedAndDeletedBases(
            final String cigarString, final int insertedBases, final int deletedBases) {
        final Cigar cigar = TextCigarCodec.decode(cigarString);
        Assert.assertEquals(SequenceUtil.countInsertedBases(cigar), insertedBases);
        Assert.assertEquals(SequenceUtil.countDeletedBases(cigar), deletedBases);
    }

    @DataProvider(name = "countInsertedAndDeletedBasesTestCases")
    public Object[][] countInsertedAndDeletedBasesTestCases() {
        return new Object[][] {
            {"2H2S32M", 0, 0},
            {"2H2S32M12I2M2I3M", 14, 0},
            {"32M2D10M", 0, 2},
            {"32M2D10M3D1M", 0, 5},
            {"2H2S32M12I2M3D1M2I3M2D1M", 14, 5}
        };
    }

    @DataProvider(name = "testKmerGenerationTestCases")
    public Object[][] testKmerGenerationTestCases() {
        return new Object[][] {
            {0, new String[] {""}},
            {1, new String[] {"A", "C", "G", "T"}},
            {
                2,
                new String[] {
                    "AA", "AC", "AG", "AT", "CA", "CC", "CG", "CT", "GA", "GC", "GG", "GT", "TA", "TC", "TG", "TT"
                }
            }
        };
    }

    @Test(dataProvider = "testKmerGenerationTestCases")
    public void testKmerGeneration(final int length, final String[] expectedKmers) {
        final Set<String> actualSet = new HashSet<>();
        for (final byte[] kmer : SequenceUtil.generateAllKmers(length)) {
            actualSet.add(StringUtil.bytesToString(kmer));
        }
        final Set<String> expectedSet = new HashSet<>(Arrays.asList(expectedKmers));
        Assert.assertTrue(actualSet.equals(expectedSet));
    }

    @DataProvider(name = "testBisulfiteConversionDataProvider")
    public Object[][] testBisulfiteConversionDataProvider() {
        // C ref -> T read on the positive strand, and G ref -> A read on the negative strand
        return new Object[][] {
            {'C', 'T', false, false},
            {'C', 'A', false, false},
            {'C', 'C', false, false},
            {'T', 'C', true, false},
            {'G', 'T', false, false},
            {'G', 'A', false, false},
            {'G', 'G', false, false},
            {'A', 'G', false, true}
        };
    }

    @Test(dataProvider = "testBisulfiteConversionDataProvider")
    public void testBisulfiteConversion(
            final char readBase, final char refBase, final boolean posStrandExpected, final boolean negStrandExpected) {
        final boolean posStrand = SequenceUtil.isBisulfiteConverted((byte) readBase, (byte) refBase, false);
        Assert.assertEquals(posStrand, posStrandExpected);
        final boolean negStrand = SequenceUtil.isBisulfiteConverted((byte) readBase, (byte) refBase, true);
        Assert.assertEquals(negStrand, negStrandExpected);
    }

    @Test(dataProvider = "basesEqualDataProvider")
    public void testBasesEqual(
            final char base1,
            final char base2,
            final boolean expectedB1EqualsB2,
            final boolean expectedB1ReadMatchesB2Ref,
            final boolean expectedB2ReadMatchesB1Ref) {

        final char[] base1UcLc = new char[] {toUpperCase(base1), toLowerCase(base1)};
        final char[] base2UcLc = new char[] {toUpperCase(base2), toLowerCase(base2)};
        // Test over all permutations - uc vs uc, uc vs lc, lc vs uc, lc vs lc
        for (char theBase1 : base1UcLc) {
            for (char theBase2 : base2UcLc) {
                // for equality, order should not matter
                final boolean b1EqualsB2 = SequenceUtil.basesEqual((byte) theBase1, (byte) theBase2);
                Assert.assertEquals(
                        b1EqualsB2,
                        expectedB1EqualsB2,
                        "basesEqual test failed for '" + theBase1 + "' vs. '" + theBase2 + "'");
                final boolean b2EqualsB1 = SequenceUtil.basesEqual((byte) theBase2, (byte) theBase1);
                Assert.assertEquals(
                        b2EqualsB1,
                        expectedB1EqualsB2,
                        "basesEqual test failed for '" + theBase1 + "' vs. '" + theBase2 + "'");

                // for ambiguous read/ref matching, the order does matter
                final boolean b1ReadMatchesB2Ref =
                        SequenceUtil.readBaseMatchesRefBaseWithAmbiguity((byte) theBase1, (byte) theBase2);
                Assert.assertEquals(
                        b1ReadMatchesB2Ref,
                        expectedB1ReadMatchesB2Ref,
                        "readBaseMatchesRefBaseWithAmbiguity test failed for '" + theBase1 + "' vs. '" + theBase2
                                + "'");
                final boolean b2ReadMatchesB1Ref =
                        SequenceUtil.readBaseMatchesRefBaseWithAmbiguity((byte) theBase2, (byte) theBase1);
                Assert.assertEquals(
                        b2ReadMatchesB1Ref,
                        expectedB2ReadMatchesB1Ref,
                        "readBaseMatchesRefBaseWithAmbiguity test failed for '" + theBase1 + "' vs. '" + theBase2
                                + "'");
            }
        }
    }

    /*
     * For reference:
     * M = A|C
     * R = A|G
     * W = A|T
     * S = C|G
     * Y = C|T
     * K = G|T
     * V = A|C|G
     * H = A|C|T
     * D = A|G|T
     * B = C|G|T
     * N = A|C|G|T
     */
    @DataProvider(name = "basesEqualDataProvider")
    public Object[][] testBasesEqualDataProvider() {
        return new Object[][] {
            {'A', 'A', true, true, true},
            {'A', 'C', false, false, false},
            {'A', 'G', false, false, false},
            {'A', 'T', false, false, false},
            {'A', 'M', false, true, false},
            {'A', 'R', false, true, false},
            {'A', 'W', false, true, false},
            {'A', 'S', false, false, false},
            {'A', 'Y', false, false, false},
            {'A', 'K', false, false, false},
            {'A', 'V', false, true, false},
            {'A', 'H', false, true, false},
            {'A', 'D', false, true, false},
            {'A', 'B', false, false, false},
            {'A', 'N', false, true, false},
            {'C', 'C', true, true, true},
            {'C', 'G', false, false, false},
            {'C', 'T', false, false, false},
            {'C', 'M', false, true, false},
            {'C', 'R', false, false, false},
            {'C', 'W', false, false, false},
            {'C', 'S', false, true, false},
            {'C', 'Y', false, true, false},
            {'C', 'K', false, false, false},
            {'C', 'V', false, true, false},
            {'C', 'H', false, true, false},
            {'C', 'D', false, false, false},
            {'C', 'N', false, true, false},
            {'G', 'G', true, true, true},
            {'G', 'T', false, false, false},
            {'G', 'M', false, false, false},
            {'G', 'R', false, true, false},
            {'G', 'W', false, false, false},
            {'G', 'S', false, true, false},
            {'G', 'Y', false, false, false},
            {'G', 'K', false, true, false},
            {'G', 'V', false, true, false},
            {'G', 'H', false, false, false},
            {'G', 'N', false, true, false},
            {'T', 'T', true, true, true},
            {'T', 'W', false, true, false},
            {'T', 'Y', false, true, false},
            {'T', 'V', false, false, false},
            {'M', 'T', false, false, false},
            {'M', 'M', true, true, true},
            {'M', 'R', false, false, false},
            {'M', 'W', false, false, false},
            {'M', 'S', false, false, false},
            {'M', 'Y', false, false, false},
            {'M', 'V', false, true, false},
            {'M', 'N', false, true, false},
            {'R', 'T', false, false, false},
            {'R', 'R', true, true, true},
            {'R', 'W', false, false, false},
            {'R', 'S', false, false, false},
            {'R', 'Y', false, false, false},
            {'R', 'V', false, true, false},
            {'W', 'W', true, true, true},
            {'W', 'Y', false, false, false},
            {'S', 'T', false, false, false},
            {'S', 'W', false, false, false},
            {'S', 'S', true, true, true},
            {'S', 'Y', false, false, false},
            {'S', 'V', false, true, false},
            {'Y', 'Y', true, true, true},
            {'K', 'T', false, false, true},
            {'K', 'M', false, false, false},
            {'K', 'R', false, false, false},
            {'K', 'W', false, false, false},
            {'K', 'S', false, false, false},
            {'K', 'Y', false, false, false},
            {'K', 'K', true, true, true},
            {'K', 'V', false, false, false},
            {'K', 'N', false, true, false},
            {'V', 'W', false, false, false},
            {'V', 'Y', false, false, false},
            {'V', 'V', true, true, true},
            {'H', 'T', false, false, true},
            {'H', 'M', false, false, true},
            {'H', 'R', false, false, false},
            {'H', 'W', false, false, true},
            {'H', 'S', false, false, false},
            {'H', 'Y', false, false, true},
            {'H', 'K', false, false, false},
            {'H', 'V', false, false, false},
            {'H', 'H', true, true, true},
            {'H', 'N', false, true, false},
            {'D', 'G', false, false, true},
            {'D', 'T', false, false, true},
            {'D', 'M', false, false, false},
            {'D', 'R', false, false, true},
            {'D', 'W', false, false, true},
            {'D', 'S', false, false, false},
            {'D', 'Y', false, false, false},
            {'D', 'K', false, false, true},
            {'D', 'V', false, false, false},
            {'D', 'H', false, false, false},
            {'D', 'D', true, true, true},
            {'D', 'N', false, true, false},
            {'B', 'C', false, false, true},
            {'B', 'G', false, false, true},
            {'B', 'T', false, false, true},
            {'B', 'M', false, false, false},
            {'B', 'R', false, false, false},
            {'B', 'W', false, false, false},
            {'B', 'S', false, false, true},
            {'B', 'Y', false, false, true},
            {'B', 'K', false, false, true},
            {'B', 'V', false, false, false},
            {'B', 'H', false, false, false},
            {'B', 'D', false, false, false},
            {'B', 'B', true, true, true},
            {'B', 'N', false, true, false},
            {'N', 'T', false, false, true},
            {'N', 'R', false, false, true},
            {'N', 'W', false, false, true},
            {'N', 'S', false, false, true},
            {'N', 'Y', false, false, true},
            {'N', 'V', false, false, true},
            {'N', 'N', true, true, true}
        };
    }

    private char toUpperCase(final char base) {
        return base > 90 ? (char) (base - 32) : base;
    }

    private char toLowerCase(final char base) {
        return (char) (toUpperCase(base) + 32);
    }

    @Test(dataProvider = "testGetSamReadNameFromFastqHeader")
    public void testGetSamReadNameFromFastqHeader(final String fastqHeader, final String expectedSamReadName) {
        Assert.assertEquals(SequenceUtil.getSamReadNameFromFastqHeader(fastqHeader), expectedSamReadName);
    }

    @DataProvider(name = "testGetSamReadNameFromFastqHeader")
    public Object[][] testGetSamReadNameFromFastqHeaderTestCases() {
        return new Object[][] {
            {"Simple:Name", "Simple:Name"},
            {"Simple:Name", "Simple:Name"},
            {"Name/1", "Name"},
            {"Name/2", "Name"},
            {"Name/3", "Name/3"},
            {"Simple:Name Blank", "Simple:Name"},
            {"Simple:Name Blank /1", "Simple:Name"},
            {"Name/1/2", "Name"}
        };
    }

    @Test
    public void testCalculateNmTag() {
        final File TEST_DIR = new File("src/test/resources/htsjdk/samtools/SequenceUtil");
        final File referenceFile = new File(TEST_DIR, "reference_with_lower_and_uppercase.fasta");
        final File samFile = new File(TEST_DIR, "upper_and_lowercase_read.sam");

        SamReader reader = SamReaderFactory.makeDefault().open(samFile);
        ReferenceSequenceFile ref = ReferenceSequenceFileFactory.getReferenceSequenceFile(referenceFile);

        reader.iterator().stream().forEach(r -> {
            Integer nm = SequenceUtil.calculateSamNmTag(
                    r, ref.getSequence(r.getContig()).getBases());
            String md = r.getStringAttribute(SAMTag.MD);
            Assert.assertEquals(
                    r.getIntegerAttribute(SAMTag.NM), nm, "problem with NM in read \'" + r.getReadName() + "\':");
            SequenceUtil.calculateMdAndNmTags(r, ref.getSequence(r.getContig()).getBases(), true, true);

            Assert.assertEquals(
                    r.getIntegerAttribute(SAMTag.NM), nm, "problem with NM in read \'" + r.getReadName() + "\':");
            if (md != null) {
                Assert.assertEquals(
                        r.getStringAttribute(SAMTag.MD), md, "problem with MD in read \'" + r.getReadName() + "\':");
            }
        });
    }

    @DataProvider(name = "testNmFromCigarProvider")
    Object[][] testNmFromCigar() {
        return new Object[][] {
            {"1M", 0},
            {"1S1D", 1},
            {"1H3X", 3},
            {"1H5=3M2X", 2},
            {"5P5M", 0},
            {"5S8I", 8}
        };
    }

    @Test(dataProvider = "testNmFromCigarProvider")
    public void testNmTagFromCigar(final String cigarString, final int expectedNmValue) {
        final SAMRecord rec = new SAMRecord(null);
        rec.setReadName("test");
        rec.setCigarString(cigarString);

        Assert.assertEquals(SequenceUtil.calculateSamNmTagFromCigar(rec), expectedNmValue);
    }

    @Test
    public void testReverseComplement() {
        Assert.assertEquals(SequenceUtil.reverseComplement("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), "ZYXWVUASRQPONMLKJIHCFEDGBT");
        Assert.assertEquals(
                SequenceUtil.reverseComplement("abcdefghijklmnopqrstuvwxy"),
                "yxwvuasrqponmlkjihcfedgbt"); // missing "z" on purpose so that we test both even-lengthed and
        // odd-lengthed strings
    }

    @Test
    public void testUpperCase() {
        Assert.assertEquals(
                SequenceUtil.upperCase(StringUtil.stringToBytes("ABCDEFGHIJKLMNOPQRSTUVWXYZ")),
                StringUtil.stringToBytes("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
        Assert.assertEquals(
                SequenceUtil.upperCase(StringUtil.stringToBytes("abcdefghijklmnopqrstuvwxyz")),
                StringUtil.stringToBytes("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
        Assert.assertEquals(
                SequenceUtil.upperCase(StringUtil.stringToBytes("1234567890!@#$%^&*()")),
                StringUtil.stringToBytes("1234567890!@#$%^&*()"));
    }

    @Test
    public void testReverseQualities() {

        final byte[] qualities1 = new byte[] {10, 20, 30, 40};
        SequenceUtil.reverseQualities(qualities1);
        assertEquals(qualities1, new byte[] {40, 30, 20, 10});

        final byte[] qualities2 = {10, 20, 30};
        SequenceUtil.reverseQualities(qualities2);
        assertEquals(qualities2, new byte[] {30, 20, 10});
    }

    private void assertEquals(final byte[] actual, final byte[] expected) {
        Assert.assertEquals(actual.length, expected.length, "Arrays do not have equal lengths");

        for (int i = 0; i < actual.length; ++i) {
            Assert.assertEquals(actual[i], expected[i], "Array differ at position " + i);
        }
    }

    @Test
    public void testIsACGTN() {
        for (byte base = Byte.MIN_VALUE; base < Byte.MAX_VALUE; base++) {
            if (base == 'A' || base == 'C' || base == 'G' || base == 'T' || base == 'N') {
                Assert.assertTrue(SequenceUtil.isUpperACGTN(base));
            } else {
                Assert.assertFalse(SequenceUtil.isUpperACGTN(base));
            }
        }
    }

    @Test
    public void testIsIUPAC() {
        final String iupacString = ".aAbBcCdDgGhHkKmMnNrRsStTvVwWyY";
        for (byte code = 0; code < Byte.MAX_VALUE; code++) {
            if (iupacString.contains(new String(new char[] {(char) code}))) {
                Assert.assertTrue(SequenceUtil.isIUPAC(code));
            } else {
                Assert.assertFalse(SequenceUtil.isIUPAC(code));
            }
        }
    }

    @Test
    public void testIUPAC_CODES_STRING() {
        for (final byte code : SequenceUtil.getIUPACCodesString().getBytes()) {
            Assert.assertTrue(SequenceUtil.isIUPAC(code));
        }
    }

    @Test
    public void testIsBamReadBase() {
        final String iupacUpperCasedWithoutDot =
                "=" + SequenceUtil.getIUPACCodesString().toUpperCase().replaceAll("\\.", "N");

        for (byte code = 0; code < Byte.MAX_VALUE; code++) {
            if (iupacUpperCasedWithoutDot.contains(new String(new char[] {(char) code}))) {
                Assert.assertTrue(SequenceUtil.isBamReadBase(code));
            } else {
                Assert.assertFalse(SequenceUtil.isBamReadBase(code), "" + code);
            }
        }
        Assert.assertTrue(SequenceUtil.isBamReadBase((byte) '='));
    }

    @Test
    public void testToBamReadBases() {
        final String testInput = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_.-=";

        /**
         * This can be obtained by :
         * echo 'blah' | tr a-z A-Z | tr -c '=ABCDGHKMNRSTVWY' N
         */
        final String expected = "ABCDNNGHNNKNMNNNNRSTNVWNYNABCDNNGHNNKNMNNNNRSTNVWNYNNNN=";

        Assert.assertEquals(SequenceUtil.toBamReadBasesInPlace(testInput.getBytes()), expected.getBytes());
    }

    @DataProvider
    Object[][] testRandomBasesLength() {
        return new Object[][] {{0}, {1}, {2}, {3}, {5}, {10}, {100}, {1000}, {10000}, {100000}};
    }

    @Test(dataProvider = "testRandomBasesLength")
    void testRandomBasesLength(final int length) {
        final Random random = new Random(42);
        Assert.assertEquals(SequenceUtil.getRandomBases(random, length).length, length);
    }

    @Test(expectedExceptions = IllegalArgumentException.class)
    void testRandomBasesNegative() {
        final Random random = new Random(42);
        SequenceUtil.getRandomBases(random, -1);
    }

    @Test
    void testRandomBases() {
        final Random random = new Random(42);
        Assert.assertEquals(
                SequenceUtil.getRandomBases(random, 100),
                "GAGACTCGGATCCCCGCTTTTACCGTCTAAGCACTCAAGCTGGAGATTACCATACTTAGGCTCATGTAGCCACCCGCGCTCGTAAATTCTCGACATTCCG"
                        .getBytes());
    }

    // --- Tests for calculateMdAndNm (the focused, SAMRecord-independent method) ---

    @DataProvider(name = "calculateMdAndNmTestCases")
    Object[][] calculateMdAndNmTestCases() {
        return new Object[][] {
            // description, cigar, readBases, refBases, refOffset, alignmentStart, expectedMD, expectedNM
            {"perfect match 10M", "10M", "ACGTACGTAC", "ACGTACGTAC", 0, 1, "10", 0},
            {"single mismatch at end", "5M", "ACGTG", "ACGTA", 0, 1, "4A0", 1},
            {"mismatch at start", "5M", "TCGTA", "ACGTA", 0, 1, "0A4", 1},
            {"mismatch in middle", "5M", "ACTGA", "ACGGA", 0, 1, "2G2", 1},
            {"two mismatches at ends", "5M", "TCGTG", "ACGTA", 0, 1, "0A3A0", 2},
            {"3bp deletion", "3M3D3M", "ACGXYZ".replace("XYZ", "TAC"), "ACGNNNTAC", 0, 1, "3^NNN3", 3},
            {"2bp insertion", "3M2I3M", "ACGXXTAC", "ACGTAC", 0, 1, "6", 2},
            {"soft clip at start", "2S3M", "XXACG", "ACG", 0, 1, "3", 0},
            {"soft clip at end", "3M2S", "ACGXX", "ACG", 0, 1, "3", 0},
            {
                "alignment starts at position 5 (1-based), ref covers full contig",
                "5M",
                "ACGTA",
                "NNNNACGTA",
                0,
                5,
                "5",
                0
            },
            {"ref slice with non-zero offset", "5M", "ACGTA", "ACGTA", 4, 5, "5", 0},
            {"ref slice with offset and mismatch", "5M", "ACGTG", "ACGTA", 100, 101, "4A0", 1},
            {"deletion with ref offset", "3M2D3M", "ACGTAC", "ACGNNTAC", 10, 11, "3^NN3", 2},
            {"skip region (N in cigar)", "3M100N3M", "ACGTAC", buildRef(106, "ACG", 0, "TAC", 103), 0, 1, "6", 0},
            {"case insensitive matching", "5M", "acgta", "ACGTA", 0, 1, "5", 0},
        };
    }

    @Test(dataProvider = "calculateMdAndNmTestCases")
    void testCalculateMdAndNm(
            final String description,
            final String cigarStr,
            final String readBasesStr,
            final String refBasesStr,
            final int refOffset,
            final int alignmentStart,
            final String expectedMD,
            final int expectedNM) {
        final Cigar cigar = TextCigarCodec.decode(cigarStr);
        final Tuple<String, Integer> result = SequenceUtil.calculateMdAndNm(
                cigar.getCigarElements(), readBasesStr.getBytes(), refBasesStr.getBytes(), refOffset, alignmentStart);

        Assert.assertEquals(result.a, expectedMD, description + ": MD mismatch");
        Assert.assertEquals(result.b.intValue(), expectedNM, description + ": NM mismatch");
    }

    @Test
    void testCalculateMdAndNmMatchesLegacyMethod() {
        // Verify the new method produces identical results to the legacy SAMRecord-based method
        final byte[] ref = "ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT".getBytes();
        final SAMFileHeader header = new SAMFileHeader();
        header.addSequence(new SAMSequenceRecord("chr1", ref.length));

        final SAMRecord record = new SAMRecord(header);
        record.setReferenceName("chr1");
        record.setAlignmentStart(5);
        record.setCigarString("10M2I5M3D5M");
        // Read: 10 matching + 2 inserted + 5 matching + 3 deleted + 5 matching
        final byte[] readBases = new byte[22]; // 10+2+5+5
        System.arraycopy(ref, 4, readBases, 0, 10); // 10M
        readBases[10] = 'A';
        readBases[11] = 'C'; // 2I
        System.arraycopy(ref, 14, readBases, 12, 5); // 5M
        System.arraycopy(ref, 22, readBases, 17, 5); // 5M (after 3D)
        record.setReadBases(readBases);

        // Use legacy method
        SequenceUtil.calculateMdAndNmTags(record, ref, true, true);
        final String legacyMD = record.getStringAttribute(SAMTag.MD);
        final int legacyNM = record.getIntegerAttribute(SAMTag.NM);

        // Use new method
        final Tuple<String, Integer> result =
                SequenceUtil.calculateMdAndNm(record.getCigar().getCigarElements(), readBases, ref, 0, 5);

        Assert.assertEquals(result.a, legacyMD, "MD should match legacy method");
        Assert.assertEquals(result.b.intValue(), legacyNM, "NM should match legacy method");
    }

    /** Build a reference string of the given length, placing sequences at specified offsets. */
    private static String buildRef(
            final int length, final String seq1, final int offset1, final String seq2, final int offset2) {
        final char[] ref = new char[length];
        java.util.Arrays.fill(ref, 'N');
        seq1.getChars(0, seq1.length(), ref, offset1);
        seq2.getChars(0, seq2.length(), ref, offset2);
        return new String(ref);
    }
}
