# Note: Tests for the TitlesAlignments class are in blast/test_titles.py
#       and diamond/test_titles.py because that class needs a concrete
#       (iterable) dark.alignments.ReadsAlignments class passed to its
#       __init__.  The tests below test the simpler dark.titles classes,
#       TitleAlignment and TitleAlignments.

from collections import Counter
from unittest import TestCase

from dark.titles import TitleAlignment, TitleAlignments
from dark.reads import Read
from dark.hsp import HSP, LSP


class TestTitleAlignment(TestCase):
    """
    Test the TitleAlignment class.
    """

    def testExpectedAttributes(self):
        """
        An instance of TitleAlignment must have the expected attributes.
        """
        read = Read("id", "AAA")
        titleAlignment = TitleAlignment(read, [])
        self.assertEqual(read, titleAlignment.read)
        self.assertEqual([], titleAlignment.hsps)

    def testToDict(self):
        """
        The toDict method must return the expected result.
        """
        read = Read("the-id", "AAA")
        hsp1 = HSP(
            0,
            readStart=1,
            readEnd=2,
            readStartInSubject=3,
            readEndInSubject=4,
            subjectStart=5,
            subjectEnd=6,
            readMatchedSequence="aaa",
            subjectMatchedSequence="ccc",
            readFrame=7,
            subjectFrame=8,
            identicalCount=9,
            percentIdentical=31.3,
            positiveCount=10,
            percentPositive=4.5,
        )
        hsp2 = HSP(
            10,
            readStart=11,
            readEnd=12,
            readStartInSubject=13,
            readEndInSubject=14,
            subjectStart=15,
            subjectEnd=16,
            readMatchedSequence="ggg",
            subjectMatchedSequence="ttt",
            readFrame=17,
            subjectFrame=18,
            identicalCount=19,
            percentIdentical=32.3,
            positiveCount=20,
            percentPositive=4.6,
        )
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])

        self.assertEqual(
            {
                "hsps": [
                    {
                        "score": 0,
                        "readStart": 1,
                        "readEnd": 2,
                        "readStartInSubject": 3,
                        "readEndInSubject": 4,
                        "subjectStart": 5,
                        "subjectEnd": 6,
                        "readFrame": 7,
                        "subjectFrame": 8,
                        "identicalCount": 9,
                        "percentIdentical": 31.3,
                        "positiveCount": 10,
                        "percentPositive": 4.5,
                        "readMatchedSequence": "aaa",
                        "subjectMatchedSequence": "ccc",
                    },
                    {
                        "score": 10,
                        "readStart": 11,
                        "readEnd": 12,
                        "readStartInSubject": 13,
                        "readEndInSubject": 14,
                        "subjectStart": 15,
                        "subjectEnd": 16,
                        "readFrame": 17,
                        "subjectFrame": 18,
                        "identicalCount": 19,
                        "percentIdentical": 32.3,
                        "positiveCount": 20,
                        "percentPositive": 4.6,
                        "readMatchedSequence": "ggg",
                        "subjectMatchedSequence": "ttt",
                    },
                ],
                "read": {
                    "id": "the-id",
                    "quality": None,
                    "sequence": "AAA",
                },
            },
            titleAlignment.toDict(),
        )


class TestTitleAlignments(TestCase):
    """
    Test the TitleAlignments class.
    """

    def testExpectedAttributes(self):
        """
        An instance of TitleAlignments must have the expected attributes.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        self.assertEqual("subject title", titleAlignments.subjectTitle)
        self.assertEqual(55, titleAlignments.subjectLength)
        self.assertEqual([], titleAlignments)

    def testAddAlignment(self):
        """
        It must be possible to add an alignment to an instance of
        TitleAlignments.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id", "AAA")
        titleAlignment = TitleAlignment(read, [])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(read, titleAlignments[0].read)
        self.assertEqual([], titleAlignments[0].hsps)

    def testHSPs(self):
        """
        The hsps function must produce a list of all HSPs.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(14)
        hsp3 = HSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            [7, 14, 21], [hsp.score.score for hsp in titleAlignments.hsps()]
        )

    def testReadsEmpty(self):
        """
        The reads function must return an empty Reads instance if there are no
        reads for the title.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        self.assertEqual(0, len(list(titleAlignments.reads())))

    def testReads(self):
        """
        The reads function must return a Reads instance with the reads for
        the title.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(14)
        hsp3 = HSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read1 = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read1, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read2 = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read2, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual([read1, read2], list(titleAlignments.reads()))

    def testReadCountZero(self):
        """
        The readCount function must return zero if no reads matched a title.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        self.assertEqual(0, titleAlignments.readCount())

    def testReadCount(self):
        """
        The readCount function must indicate how many reads matched a title.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(14)
        hsp3 = HSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(2, titleAlignments.readCount())

    def testHspCountZero(self):
        """
        The hspCount function must return zero if no reads matched a title.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        self.assertEqual(0, titleAlignments.hspCount())

    def testHspCount(self):
        """
        The hspCount function must indicate how many HSPs were found in
        total for all the alignments to a title.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(14)
        hsp3 = HSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(3, titleAlignments.hspCount())

    def testMedianScoreWithNoAlignments(self):
        """
        The medianScore function must raise IndexError (due to no inputs)
        if there are no alignments matching a title.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        error = "^arg is an empty sequence$"
        self.assertRaisesRegex(ValueError, error, titleAlignments.medianScore)

    def testMedianScoreWithNoHsps(self):
        """
        The medianScore function must raise ValueError if there are no HSPs.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [])
        titleAlignments.addAlignment(titleAlignment)
        error = "^arg is an empty sequence$"
        self.assertRaisesRegex(ValueError, error, titleAlignments.medianScore)

    def testMedianScoreOfTwo(self):
        """
        The medianScore function must return the median score for the HSPs in
        all the alignments matching a title when given 2 scores.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(15)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(11, titleAlignments.medianScore())

    def testMedianScoreOfThree(self):
        """
        The medianScore function must return the median score for the HSPs in
        all the alignments matching a title when given 3 scores.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(15)
        hsp3 = HSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(15, titleAlignments.medianScore())

    def testBestHspWithNoHsps(self):
        """
        The bestHsp function must raise ValueError if there are no HSPs.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [])
        titleAlignments.addAlignment(titleAlignment)
        error = "^max\\(\\) arg is an empty sequence$"
        self.assertRaisesRegex(ValueError, error, titleAlignments.bestHsp)

    def testBestHsp(self):
        """
        The bestHsp function must return the HSP with the best score for all
        the HSPs for all the alignments matching a title.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(15)
        hsp3 = HSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(hsp3, titleAlignments.bestHsp())

    def testWorstHspWithNoHsps(self):
        """
        The worstHsp function must raise ValueError if there are no HSPs.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [])
        titleAlignments.addAlignment(titleAlignment)
        error = "^min\\(\\) arg is an empty sequence$"
        self.assertRaisesRegex(ValueError, error, titleAlignments.worstHsp)

    def testWorstHsp(self):
        """
        The worstHsp function must return the HSP with the worst score for all
        the HSPs for all the alignments matching a title.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(15)
        hsp3 = HSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(hsp1, titleAlignments.worstHsp())

    def testBetterThanFalse(self):
        """
        The hasScoreBetterThan function must return False if there is no HSP
        with a score better than the passed value.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(15)
        hsp3 = HSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertFalse(titleAlignments.hasScoreBetterThan(21))

    def testBetterThanTrue(self):
        """
        The hasScoreBetterThan function must return True if there is an HSP
        with a score better than the passed value.
        """
        hsp1 = HSP(7)
        hsp2 = HSP(15)
        hsp3 = HSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertTrue(titleAlignments.hasScoreBetterThan(20))

    def testCoverageNoReads(self):
        """
        The coverage method must return zero when a title alignments has no
        alignments (and therefore no coverage).
        """
        titleAlignments = TitleAlignments("subject title", 100)
        self.assertEqual(0.0, titleAlignments.coverage())

    def testFullCoverage(self):
        """
        The coverage method must return the correct value when the title is
        fully covered by its reads.
        """
        hsp1 = HSP(7, subjectStart=0, subjectEnd=50)
        hsp2 = HSP(8, subjectStart=50, subjectEnd=100)
        titleAlignments = TitleAlignments("subject title", 100)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(1.0, titleAlignments.coverage())

    def testPartialCoverage(self):
        """
        The coverage method must return the correct value when the title is
        partially covered by its reads.
        """
        hsp1 = HSP(7, subjectStart=10, subjectEnd=20)
        hsp2 = HSP(15, subjectStart=30, subjectEnd=40)
        hsp3 = HSP(21, subjectStart=50, subjectEnd=60)
        titleAlignments = TitleAlignments("subject title", 100)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(0.3, titleAlignments.coverage())

    def testFullCoverageCounts(self):
        """
        The coverageCounts method must return the correct result when the title
        is fully covered by its reads.
        """
        hsp1 = HSP(7, subjectStart=0, subjectEnd=5)
        hsp2 = HSP(8, subjectStart=5, subjectEnd=10)
        titleAlignments = TitleAlignments("subject title", 10)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        c = Counter([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
        self.assertEqual(c, titleAlignments.coverageCounts())

    def testCoverageCounts(self):
        """
        The coverageCounts method must return the correct results when the
        title is partially covered by its reads.
        """
        hsp1 = HSP(7, subjectStart=1, subjectEnd=2)
        hsp2 = HSP(15, subjectStart=3, subjectEnd=4)
        hsp3 = HSP(21, subjectStart=5, subjectEnd=6)
        titleAlignments = TitleAlignments("subject title", 10)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        c = Counter([1, 3, 5])
        self.assertEqual(c, titleAlignments.coverageCounts())

    def testCoverageCountsOverlap(self):
        """
        The coverageCounts method must return the correct results when the
        title is partially covered by its reads that overlap.
        """
        hsp1 = HSP(7, subjectStart=1, subjectEnd=2)
        hsp2 = HSP(15, subjectStart=3, subjectEnd=6)
        hsp3 = HSP(21, subjectStart=5, subjectEnd=6)
        titleAlignments = TitleAlignments("subject title", 10)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        c = Counter([1, 3, 4, 5, 5])
        self.assertEqual(c, titleAlignments.coverageCounts())

    def testCoverageInfoNoReads(self):
        """
        When a title has no reads aligned to it, the coverageInfo method
        must return an empty result.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        coverage = titleAlignments.coverageInfo()
        self.assertEqual({}, coverage)

    def testCoverageInfoOneReadWithOneHSP(self):
        """
        When a title has one read with one HSP aligned to it, the coverageInfo
        method must return just the indices and bases from that read.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        hsp = HSP(15, subjectStart=3, subjectEnd=6, readMatchedSequence="CGT")
        read = Read("id1", "AAACGT")
        titleAlignment = TitleAlignment(read, [hsp])
        titleAlignments.addAlignment(titleAlignment)
        coverage = titleAlignments.coverageInfo()
        self.assertEqual(
            {
                3: [(15, "C")],
                4: [(15, "G")],
                5: [(15, "T")],
            },
            coverage,
        )

    def testCoverageInfoOneReadWithTwoHSPs(self):
        """
        When a title has one read with two HSPs aligned to it, the coverageInfo
        method must return the correct indices and bases from that read.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        hsp1 = HSP(15, subjectStart=1, subjectEnd=4, readMatchedSequence="A-A")
        hsp2 = HSP(10, subjectStart=3, subjectEnd=6, readMatchedSequence="CGT")
        read = Read("id1", "AAACGT")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        coverage = titleAlignments.coverageInfo()
        self.assertEqual(
            {
                1: [(15, "A")],
                2: [(15, "-")],
                3: [(15, "A"), (10, "C")],
                4: [(10, "G")],
                5: [(10, "T")],
            },
            coverage,
        )

    def testCoverageInfoTwoReadsWithThreeHSPs(self):
        """
        When a title has two reads (one with two HSPs, one with one) aligned
        to it, the coverageInfo method must return the correct indices and
        bases from the read.
        """
        titleAlignments = TitleAlignments("subject title", 55)

        # First read.
        hsp1 = HSP(15, subjectStart=1, subjectEnd=4, readMatchedSequence="A-A")
        hsp2 = HSP(10, subjectStart=3, subjectEnd=6, readMatchedSequence="CGT")
        read = Read("id1", "AAACGT")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)

        # Second read.
        hsp1 = HSP(20, subjectStart=5, subjectEnd=10, readMatchedSequence="CGGTA")
        read = Read("id2", "AAACGTCGGTAAAA")
        titleAlignment = TitleAlignment(read, [hsp1])
        titleAlignments.addAlignment(titleAlignment)

        coverage = titleAlignments.coverageInfo()
        self.assertEqual(
            {
                1: [(15, "A")],
                2: [(15, "-")],
                3: [(15, "A"), (10, "C")],
                4: [(10, "G")],
                5: [(10, "T"), (20, "C")],
                6: [(20, "G")],
                7: [(20, "G")],
                8: [(20, "T")],
                9: [(20, "A")],
            },
            coverage,
        )

    def testResidueCountsNoReads(self):
        """
        When a title has no reads aligned to it, the residueCounts method
        must return an empty result.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        counts = titleAlignments.residueCounts()
        self.assertEqual(0, len(counts))

    def testResidueCountsUnknownCaseConversion(self):
        """
        The residueCounts method must raise a ValueError when asked to do an
        unknown case conversion.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        error = "convertCaseTo must be one of 'none', 'lower', or 'upper'"
        self.assertRaisesRegex(
            ValueError, error, titleAlignments.residueCounts, convertCaseTo="xxx"
        )

    def testResidueCountsOneReadOneHSP(self):
        """
        The residueCounts method must return the correct result when just one
        read with one HSP is aligned to a title.
        """
        read = Read("id", "ACGT")
        hsp = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=0,
            readEndInSubject=4,
            subjectStart=0,
            subjectEnd=4,
            readMatchedSequence="ACGT",
            subjectMatchedSequence="ACGT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read, [hsp])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                0: {"A": 1},
                1: {"C": 1},
                2: {"G": 1},
                3: {"T": 1},
            },
            titleAlignments.residueCounts(),
        )

    def testResidueCountsOneReadOneHSPPartialMatch(self):
        """
        The residueCounts method must return the correct result when just one
        read with one HSP is aligned to a title and only part of the read
        matched the subject (all the read bases are still counted and
        returned).
        """
        read = Read("id", "ACGT")
        hsp = HSP(
            33,
            readStart=0,
            readEnd=2,
            readStartInSubject=0,
            readEndInSubject=4,
            subjectStart=0,
            subjectEnd=4,
            readMatchedSequence="ACGT",
            subjectMatchedSequence="ACGT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read, [hsp])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                0: {"A": 1},
                1: {"C": 1},
                2: {"G": 1},
                3: {"T": 1},
            },
            titleAlignments.residueCounts(),
        )

    def testResidueCountsOneReadTwoHSPsAtStartOfSubject(self):
        """
        The residueCounts method must return the correct result when just one
        read with two HSPs is aligned to a title and the leftmost HSP is
        aligned with the left edge of the subject.

        HSP1:       ACGT
        HSP2:        CGTT
        """
        read = Read("id", "ACGT")
        hsp1 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=0,
            readEndInSubject=4,
            subjectStart=0,
            subjectEnd=4,
            readMatchedSequence="ACGT",
            subjectMatchedSequence="ACGT",
        )
        hsp2 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=1,
            readEndInSubject=5,
            subjectStart=1,
            subjectEnd=5,
            readMatchedSequence="CGTT",
            subjectMatchedSequence="CGTT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                0: {"A": 1},
                1: {"C": 2},
                2: {"G": 2},
                3: {"T": 2},
                4: {"T": 1},
            },
            titleAlignments.residueCounts(),
        )

    def testResidueCountsOneReadTwoHSPsNotAtStartOfSubject(self):
        """
        The residueCounts method must return the correct result when just one
        read with two HSPs is aligned to a title and the leftmost HSP is not
        aligned with the left edge of the subject.

        HSP1:       ACGT
        HSP2:        CGTT
        """
        read = Read("id", "ACGT")
        hsp1 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=10,
            readEndInSubject=14,
            subjectStart=10,
            subjectEnd=14,
            readMatchedSequence="ACGT",
            subjectMatchedSequence="ACGT",
        )
        hsp2 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=11,
            readEndInSubject=15,
            subjectStart=11,
            subjectEnd=15,
            readMatchedSequence="CGTT",
            subjectMatchedSequence="CGTT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                10: {"A": 1},
                11: {"C": 2},
                12: {"G": 2},
                13: {"T": 2},
                14: {"T": 1},
            },
            titleAlignments.residueCounts(),
        )

    def testResidueCountsNoCaseConversion(self):
        """
        The residueCounts method must return the correct result when asked not
        to convert case.

        HSP1:       AcgT
        HSP2:        CGTT
        """
        read = Read("id", "ACGT")
        hsp1 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=10,
            readEndInSubject=14,
            subjectStart=10,
            subjectEnd=14,
            readMatchedSequence="AcgT",
            subjectMatchedSequence="ACGT",
        )
        hsp2 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=11,
            readEndInSubject=15,
            subjectStart=11,
            subjectEnd=15,
            readMatchedSequence="CGTT",
            subjectMatchedSequence="CGTT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                10: {"A": 1},
                11: {"C": 1, "c": 1},
                12: {"G": 1, "g": 1},
                13: {"T": 2},
                14: {"T": 1},
            },
            titleAlignments.residueCounts(convertCaseTo="none"),
        )

    def testResidueCountsCaseConvertLower(self):
        """
        The residueCounts method must return the correct result when asked to
        convert residues to lower case.

        HSP1:       AcgT
        HSP2:        CGTT
        """
        read = Read("id", "ACGT")
        hsp1 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=10,
            readEndInSubject=14,
            subjectStart=10,
            subjectEnd=14,
            readMatchedSequence="AcgT",
            subjectMatchedSequence="ACGT",
        )
        hsp2 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=11,
            readEndInSubject=15,
            subjectStart=11,
            subjectEnd=15,
            readMatchedSequence="CGTT",
            subjectMatchedSequence="CGTT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                10: {"a": 1},
                11: {"c": 2},
                12: {"g": 2},
                13: {"t": 2},
                14: {"t": 1},
            },
            titleAlignments.residueCounts(convertCaseTo="lower"),
        )

    def testResidueCountsCaseConvertUpper(self):
        """
        The residueCounts method must return the correct result when asked to
        convert residues to upper case.

        HSP1:       AcgT
        HSP2:        CGTT
        """
        read = Read("id", "ACGT")
        hsp1 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=10,
            readEndInSubject=14,
            subjectStart=10,
            subjectEnd=14,
            readMatchedSequence="AcgT",
            subjectMatchedSequence="ACGT",
        )
        hsp2 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=11,
            readEndInSubject=15,
            subjectStart=11,
            subjectEnd=15,
            readMatchedSequence="CGTT",
            subjectMatchedSequence="CGTT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                10: {"A": 1},
                11: {"C": 2},
                12: {"G": 2},
                13: {"T": 2},
                14: {"T": 1},
            },
            titleAlignments.residueCounts(convertCaseTo="upper"),
        )

    def testResidueCountsCaseConvertUpperIsDefault(self):
        """
        The residueCounts method must convert to uppercase by default.

        HSP1:       AcgT
        HSP2:        CGTT
        """
        read = Read("id", "ACGT")
        hsp1 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=10,
            readEndInSubject=14,
            subjectStart=10,
            subjectEnd=14,
            readMatchedSequence="AcgT",
            subjectMatchedSequence="ACGT",
        )
        hsp2 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=11,
            readEndInSubject=15,
            subjectStart=11,
            subjectEnd=15,
            readMatchedSequence="CGTT",
            subjectMatchedSequence="CGTT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                10: {"A": 1},
                11: {"C": 2},
                12: {"G": 2},
                13: {"T": 2},
                14: {"T": 1},
            },
            titleAlignments.residueCounts(),
        )

    def testResidueCountsTwoReadsTwoHSPsLeftOverhang(self):
        """
        The residueCounts method must return the correct result when two
        reads, each with one HSP are aligned to a title and the leftmost HSP
        is aligned before the left edge of the subject (i.e, will include
        negative subject offsets).

        Subject:      GTT
        HSP1:       ACGT
        HSP2:        CGTT
        """
        read1 = Read("id", "ACGT")
        hsp1 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=-2,
            readEndInSubject=2,
            subjectStart=0,
            subjectEnd=2,
            readMatchedSequence="GT",
            subjectMatchedSequence="GT",
        )
        read2 = Read("id", "CGTT")
        hsp2 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=-1,
            readEndInSubject=3,
            subjectStart=0,
            subjectEnd=3,
            readMatchedSequence="GTT",
            subjectMatchedSequence="GTT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read1, [hsp1])
        titleAlignments.addAlignment(titleAlignment)
        titleAlignment = TitleAlignment(read2, [hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                -2: {"A": 1},
                -1: {"C": 2},
                0: {"G": 2},
                1: {"T": 2},
                2: {"T": 1},
            },
            titleAlignments.residueCounts(),
        )

    def testResidueCountsOneReadTwoHSPsNotOverlapping(self):
        """
        The residueCounts method must return the correct result when just one
        read with two HSPs is aligned to a title and the HSPs do not overlap
        one another.

        HSP1:    ACGT
        HSP2:              CGTT
        """
        read = Read("id", "ACGT")
        hsp1 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=0,
            readEndInSubject=4,
            subjectStart=0,
            subjectEnd=4,
            readMatchedSequence="ACGT",
            subjectMatchedSequence="ACGT",
        )
        hsp2 = HSP(
            33,
            readStart=0,
            readEnd=4,
            readStartInSubject=10,
            readEndInSubject=14,
            subjectStart=10,
            subjectEnd=14,
            readMatchedSequence="CGTT",
            subjectMatchedSequence="CGTT",
        )
        titleAlignments = TitleAlignments("subject title", 55)
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(
            {
                0: {"A": 1},
                1: {"C": 1},
                2: {"G": 1},
                3: {"T": 1},
                10: {"C": 1},
                11: {"G": 1},
                12: {"T": 1},
                13: {"T": 1},
            },
            titleAlignments.residueCounts(),
        )

    def testSummaryWhenEmpty(self):
        """
        If summary is called on an instance of TitleAlignments with no
        alignments a ValueError must be raised.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        error = "^max\\(\\) arg is an empty sequence$"
        self.assertRaisesRegex(ValueError, error, titleAlignments.summary)

    def testSummary(self):
        """
        The summary method must return the correct result.
        """
        titleAlignments = TitleAlignments("subject title", 10)
        titleAlignments.addAlignment(
            TitleAlignment(
                Read("id1", "ACGT"),
                [
                    HSP(30, subjectStart=0, subjectEnd=2),
                ],
            )
        )
        titleAlignments.addAlignment(
            TitleAlignment(
                Read("id2", "ACGT"),
                [
                    HSP(55, subjectStart=2, subjectEnd=4),
                    HSP(40, subjectStart=8, subjectEnd=9),
                ],
            )
        )
        self.assertEqual(
            {
                "bestScore": 55,
                "coverage": 0.5,
                "hspCount": 3,
                "medianScore": 40,
                "readCount": 2,
                "subjectLength": 10,
                "subjectTitle": "subject title",
            },
            titleAlignments.summary(),
        )

    def testToDict(self):
        """
        The toDict method must return the expected result.
        """
        read = Read("the-id", "AAA")
        hsp1 = HSP(
            0,
            readStart=1,
            readEnd=2,
            readStartInSubject=3,
            readEndInSubject=4,
            subjectStart=5,
            subjectEnd=6,
            readMatchedSequence="aaa",
            subjectMatchedSequence="ccc",
            readFrame=7,
            subjectFrame=8,
            identicalCount=9,
            percentIdentical=17.9,
            positiveCount=10,
            percentPositive=3.9,
        )
        hsp2 = HSP(
            10,
            readStart=11,
            readEnd=12,
            readStartInSubject=13,
            readEndInSubject=14,
            subjectStart=15,
            subjectEnd=16,
            readMatchedSequence="ggg",
            subjectMatchedSequence="ttt",
            readFrame=17,
            subjectFrame=18,
            identicalCount=19,
            percentIdentical=27.9,
            positiveCount=20,
            percentPositive=3.8,
        )
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments = TitleAlignments("subject title", 10)
        titleAlignments.addAlignment(titleAlignment)

        self.assertEqual(
            {
                "subjectTitle": "subject title",
                "subjectLength": 10,
                "titleAlignments": [
                    {
                        "hsps": [
                            {
                                "score": 0,
                                "readStart": 1,
                                "readEnd": 2,
                                "readStartInSubject": 3,
                                "readEndInSubject": 4,
                                "subjectStart": 5,
                                "subjectEnd": 6,
                                "readFrame": 7,
                                "subjectFrame": 8,
                                "identicalCount": 9,
                                "percentIdentical": 17.9,
                                "positiveCount": 10,
                                "percentPositive": 3.9,
                                "readMatchedSequence": "aaa",
                                "subjectMatchedSequence": "ccc",
                            },
                            {
                                "score": 10,
                                "readStart": 11,
                                "readEnd": 12,
                                "readStartInSubject": 13,
                                "readEndInSubject": 14,
                                "subjectStart": 15,
                                "subjectEnd": 16,
                                "readFrame": 17,
                                "subjectFrame": 18,
                                "identicalCount": 19,
                                "percentIdentical": 27.9,
                                "positiveCount": 20,
                                "percentPositive": 3.8,
                                "readMatchedSequence": "ggg",
                                "subjectMatchedSequence": "ttt",
                            },
                        ],
                        "read": {
                            "id": "the-id",
                            "quality": None,
                            "sequence": "AAA",
                        },
                    },
                ],
            },
            titleAlignments.toDict(),
        )


class TestTitleAlignmentsLSP(TestCase):
    """
    Test the TitleAlignments class using LSPs. The only tests here are ones
    that depend on lower scores being better.
    """

    def testBestHsp(self):
        """
        The bestHsp function must return the HSP with the best score for the
        HSPs all the alignments matching a title.
        """
        hsp1 = LSP(7)
        hsp2 = LSP(15)
        hsp3 = LSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(hsp1, titleAlignments.bestHsp())

    def testWorstHsp(self):
        """
        The worstHsp function must return the HSP with the worst score for all
        the HSPs for all the alignments matching a title.
        """
        hsp1 = LSP(7)
        hsp2 = LSP(15)
        hsp3 = LSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(hsp3, titleAlignments.worstHsp())

    def testBetterThanFalse(self):
        """
        The hasScoreBetterThan function must return False if there is no HSP
        with a score better than the passed value.
        """
        hsp1 = LSP(7)
        hsp2 = LSP(15)
        hsp3 = LSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertFalse(titleAlignments.hasScoreBetterThan(7))

    def testBetterThanTrue(self):
        """
        The hasScoreBetterThan function must return True if there is an HSP
        with a score better than the passed value.
        """
        hsp1 = LSP(7)
        hsp2 = LSP(15)
        hsp3 = LSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertTrue(titleAlignments.hasScoreBetterThan(9))

    def testReadIdsEmpty(self):
        """
        The readIds function must return the empty set if no reads matched a
        title.
        """
        titleAlignments = TitleAlignments("subject title", 55)
        self.assertEqual(0, len(titleAlignments.readIds()))

    def testReadIds(self):
        """
        The readIds function must return the set of read ids for the alignments
        matching a title.
        """
        hsp1 = LSP(7)
        hsp2 = LSP(15)
        hsp3 = LSP(21)
        titleAlignments = TitleAlignments("subject title", 55)
        read = Read("id1", "AAA")
        titleAlignment = TitleAlignment(read, [hsp1, hsp2])
        titleAlignments.addAlignment(titleAlignment)
        read = Read("id2", "AAA")
        titleAlignment = TitleAlignment(read, [hsp3])
        titleAlignments.addAlignment(titleAlignment)
        self.assertEqual(set(["id1", "id2"]), titleAlignments.readIds())
