import abc
import collections
import math
import io
import itertools
import os
import platform
import unittest
import tempfile
import threading
import multiprocessing.resource_sharer

import pyhmmer
from pyhmmer.plan7 import HMM, Pipeline, HMMFile, HMMPressedFile, TopHits, Hit
from pyhmmer.easel import Alphabet, DigitalMSA, MSAFile, SequenceFile, TextSequence

from .utils import resource_files

class _TestSearch(metaclass=abc.ABCMeta):

    def tearDown(self):
        multiprocessing.resource_sharer.stop()
        self.assertEqual(threading.active_count(), 1, threading.enumerate())

    @abc.abstractmethod
    def get_hits(self, hmm, sequences):
        return NotImplemented

    def get_hits_multi(self, hmms, sequences):
        return [self.get_hits(hmm, sequences) for hmm in hmms]

    @staticmethod
    def table(name):
        path = resource_files(__package__).joinpath("data", "tables", name)
        return path.open()

    @staticmethod
    def hmm_file(name):
        path = resource_files(__package__).joinpath("data", "hmms", "txt", "{}.hmm".format(name))
        return HMMFile(path)

    @staticmethod
    def seqs_file(name, digital=False):
        seqs_path = resource_files(__package__).joinpath("data", "seqs", "{}.faa".format(name))
        return SequenceFile(seqs_path, digital=digital)

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_thioestherase(self):
        # $ hmmsearch data/hmm/Thioesterase.hmm data/seqs/938293.PRJEB85.HG003687.faa
        #
        # Query:       Thioesterase  [M=243]
        # Scores for complete sequences (score includes all domains):
        #    --- full sequence ---   --- best 1 domain ---    -#dom-
        #     E-value  score  bias    E-value  score  bias    exp  N  Sequence                    Description
        #     ------- ------ -----    ------- ------ -----   ---- --  --------                    -----------
        #   ------ inclusion threshold ------
        #       0.096    8.6   1.5       0.14    8.1   1.5    1.2  1  938293.PRJEB85.HG003687_113  # 108502 # 109284 # 1 # ID=8_113;partial=00;start_type=ATG;rbs_motif=GGAG/GAGG;rbs_spacer=5-10bp;gc_cont=0.341
        #
        # Domain annotation for each sequence (and alignments):
        # >> 938293.PRJEB85.HG003687_113  # 108502 # 109284 # 1 # ID=8_113;partial=00;start_type=ATG;rbs_motif=GGAG/GAGG;rbs_spacer=5-10bp;gc_cont=0.341
        #    #    score  bias  c-Evalue  i-Evalue hmmfrom  hmm to    alifrom  ali to    envfrom  env to     acc
        #  ---   ------ ----- --------- --------- ------- -------    ------- -------    ------- -------    ----
        #    1 ?    8.1   1.5   6.5e-05      0.14      79      93 ..     115     129 ..     115     129 .. 0.96
        #
        #   Alignments for each domain:
        #   == domain 1  score: 8.1 bits;  conditional E-value: 6.5e-05
        #                  Thioesterase  79 GWSfGGvlAyEmArq 93
        #                                   G+S+GG +A ++A++
        #   938293.PRJEB85.HG003687_113 115 GHSMGGSVAVAIAHE 129
        #                                   9************96 PP
        with self.hmm_file("Thioesterase") as hmm_file:
            hmm = hmm_file.read()
        with self.seqs_file("938293.PRJEB85.HG003687", digital=True) as seqs_file:
            seqs = seqs_file.read_block()

        hits = self.get_hits(hmm, seqs)
        self.assertEqual(len(hits), 1)

        hits.sort()

        hit = hits[0]
        self.assertEqual(hit.name, b"938293.PRJEB85.HG003687_113")
        self.assertAlmostEqual(hit.score, 8.6, delta=0.1)  # printed with %6.1f
        self.assertAlmostEqual(hit.bias, 1.5, delta=0.1)  # printed with  %5.1f
        self.assertAlmostEqual(hit.evalue, 0.096, delta=0.01)  # printed with %9.2g
        self.assertEqual(len(hit.domains), 1)

        domain = hit.domains[0]
        self.assertAlmostEqual(domain.score, 8.1, delta=0.1)
        self.assertAlmostEqual(domain.bias, 1.5, delta=0.1)
        self.assertAlmostEqual(domain.i_evalue, 0.14, places=2)  # printed with %9.2g
        self.assertAlmostEqual(domain.c_evalue, 6.5e-05, places=2)  # printed with %9.2g
        self.assertEqual(domain.alignment.target_from, 115)
        self.assertEqual(domain.alignment.target_to, 129)
        self.assertEqual(domain.alignment.target_length, 261)
        self.assertEqual(domain.alignment.hmm_from, 79)
        self.assertEqual(domain.alignment.hmm_to, 93)
        self.assertEqual(domain.alignment.hmm_length, 243)
        self.assertEqual(domain.env_from, 115)
        self.assertEqual(domain.env_to, 129)

        last = hit.domains[-1]
        self.assertEqual(domain.alignment.hmm_name, last.alignment.hmm_name)

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_pf02826_file(self):
        with self.hmm_file("PF02826") as hmm_file:
            hmm = hmm_file.read()
        with self.seqs_file("938293.PRJEB85.HG003687", digital=True) as seqs_file:
            hits = self.get_hits(hmm, seqs_file)
            self.assertEqual(len(hits), 22)

        hits.sort()

        with self.table("PF02826.tbl") as table:
            lines = filter(lambda line: not line.startswith("#"), table)
            for line, hit in itertools.zip_longest(lines, hits):
                fields = list(filter(None, line.strip().split(" ")))
                self.assertIsNot(line, None)
                self.assertIsNot(hit, None)
                self.assertEqual(hit.name.decode(), fields[0])
                if fields[1] == "-":
                    self.assertIs(hit.accession, None)
                else:
                    self.assertEqual(hit.accession.decode(), fields[1])
                self.assertAlmostEqual(hit.score, float(fields[5]), delta=0.1)
                self.assertAlmostEqual(hit.bias, float(fields[6]), delta=0.1)
                self.assertAlmostEqual(hit.evalue, float(fields[4]), delta=0.1)

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_pf02826_block(self):
        with self.hmm_file("PF02826") as hmm_file:
            hmm = hmm_file.read()
        with self.seqs_file("938293.PRJEB85.HG003687", digital=True) as seqs_file:
            seqs = seqs_file.read_block()

        hits = self.get_hits(hmm, seqs)
        self.assertEqual(len(hits), 22)

        hits.sort()

        with self.table("PF02826.tbl") as table:
            lines = filter(lambda line: not line.startswith("#"), table)
            for line, hit in itertools.zip_longest(lines, hits):
                fields = list(filter(None, line.strip().split(" ")))
                self.assertIsNot(line, None)
                self.assertIsNot(hit, None)
                self.assertEqual(hit.name.decode(), fields[0])
                if fields[1] == "-":
                    self.assertIs(hit.accession, None)
                else:
                    self.assertEqual(hit.accession.decode(), fields[1])
                self.assertAlmostEqual(hit.score, float(fields[5]), delta=0.1)
                self.assertAlmostEqual(hit.bias, float(fields[6]), delta=0.1)
                self.assertAlmostEqual(hit.evalue, float(fields[4]), delta=0.1)

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_rrefam(self):
        with self.seqs_file("938293.PRJEB85.HG003687", digital=True) as seqs_file:
            seqs = seqs_file.read_block()

        with self.hmm_file("RREFam") as hmm_file:
            all_hits = self.get_hits_multi(list(hmm_file), seqs)

        hits = (hit for hits in all_hits for hit in hits)
        with self.table("RREFam.tbl") as table:
            lines = filter(lambda line: not line.startswith("#"), table)
            for line, hit in itertools.zip_longest(lines, hits):
                fields = list(filter(None, line.strip().split(" ")))
                self.assertIsNot(line, None)
                self.assertIsNot(hit, None)
                self.assertEqual(hit.name.decode(), fields[0])
                if fields[1] == "-":
                    self.assertIs(hit.accession, None)
                else:
                    self.assertEqual(hit.accession.decode(), fields[1])
                self.assertAlmostEqual(hit.score, float(fields[5]), delta=0.1)
                self.assertAlmostEqual(hit.bias, float(fields[6]), delta=0.1)
                self.assertAlmostEqual(hit.evalue, float(fields[4]), delta=0.1)

        domains = [
            domain for hits in all_hits for hit in hits for domain in hit.domains
        ]
        with self.table("RREFam.domtbl") as table:
            lines = filter(lambda line: not line.startswith("#"), table)
            for line, domain in itertools.zip_longest(lines, domains):
                fields = list(filter(None, line.strip().split(" ")))
                self.assertIsNot(line, None)
                self.assertIsNot(domain, None)
                self.assertEqual(domain.hit.hits.Z, len(seqs))
                self.assertEqual(domain.hit.name.decode(), fields[0])
                self.assertAlmostEqual(domain.score, float(fields[13]), places=1)
                self.assertAlmostEqual(domain.bias, float(fields[14]), places=1)
                self.assertEqual(f"{domain.c_evalue:9.2g}", f"{float(fields[11]):9.2g}")
                self.assertEqual(f"{domain.i_evalue:9.2g}", f"{float(fields[12]):9.2g}")

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_hmm_vs_optimized_profile(self):
        with self.hmm_file("PF02826") as hmm_file:
            hmm = next(hmm_file)
            bg = pyhmmer.plan7.Background(hmm.alphabet)
            profile = pyhmmer.plan7.Profile(hmm.M, hmm.alphabet)
            profile.configure(hmm, bg, 100)
        with self.seqs_file("938293.PRJEB85.HG003687", digital=True) as seqs_file:
            seqs = seqs_file.read_block()

        hits_hmm = self.get_hits(hmm, seqs)
        self.assertEqual(len(hits_hmm), 22)

        hits_oprofile = self.get_hits(profile.to_optimized(), seqs)
        self.assertEqual(len(hits_oprofile), 22)

        for hit_hmm, hit_oprofile in itertools.zip_longest(hits_hmm, hits_oprofile):
            self.assertEqual(hit_hmm.name, hit_oprofile.name)
            self.assertEqual(hit_hmm.accession, hit_oprofile.accession)
            self.assertEqual(hit_hmm.description, hit_oprofile.description)
            self.assertEqual(hit_hmm.score, hit_oprofile.score)
            self.assertEqual(hit_hmm.pre_score, hit_oprofile.pre_score)
            self.assertEqual(hit_hmm.sum_score, hit_oprofile.sum_score)
            self.assertEqual(hit_hmm.bias, hit_oprofile.bias)
            self.assertEqual(hit_hmm.evalue, hit_oprofile.evalue)
            self.assertEqual(hit_hmm.pvalue, hit_oprofile.pvalue)
            for dom_hmm, dom_oprofile in itertools.zip_longest(
                hit_hmm.domains, hit_oprofile.domains
            ):
                self.assertEqual(dom_hmm.env_from, dom_oprofile.env_from)
                self.assertEqual(dom_hmm.env_to, dom_oprofile.env_to)
                self.assertEqual(dom_hmm.score, dom_oprofile.score)
                self.assertEqual(dom_hmm.bias, dom_oprofile.bias)
                self.assertEqual(dom_hmm.correction, dom_oprofile.correction)
                self.assertEqual(dom_hmm.envelope_score, dom_oprofile.envelope_score)
                self.assertEqual(dom_hmm.c_evalue, dom_oprofile.c_evalue)
                self.assertEqual(dom_hmm.i_evalue, dom_oprofile.i_evalue)
                self.assertEqual(dom_hmm.pvalue, dom_oprofile.pvalue)


class TestHmmsearch(_TestSearch, unittest.TestCase):
    parallel = "queries"

    @staticmethod
    def _random_sequences(n=20):
        rng = pyhmmer.easel.Randomness(42)
        alphabet = Alphabet.amino()
        return pyhmmer.easel.DigitalSequenceBlock(
            alphabet,
            [
                pyhmmer.easel.DigitalSequence.sample(alphabet, 200, rng)
                for _ in range(10)
            ]
        )

    def get_hits(self, hmm, seqs):
        return list(pyhmmer.hmmsearch(hmm, seqs, parallel=self.parallel))[0]

    def get_hits_multi(self, hmms, seqs):
        return list(pyhmmer.hmmsearch(hmms, seqs, parallel=self.parallel))

    def test_callback_error(self):

        class MyException(Exception):
            pass

        def callback(hmm, total):
            raise MyException("oopsie")

        rng = pyhmmer.easel.Randomness(42)
        alphabet = Alphabet.amino()
        hmm = HMM.sample(alphabet, 100, rng)
        seqs = self._random_sequences()

        hits = pyhmmer.hmmsearch(hmm, seqs, cpus=1, callback=callback, parallel=self.parallel)
        with self.assertRaises(MyException):
            hit = next(hits)

        hits = pyhmmer.hmmsearch(hmm, seqs, cpus=2, callback=callback, parallel=self.parallel)
        with self.assertRaises(MyException):
            hit = next(hits)

    def test_background_error(self):
        # check that errors occuring in worker threads are recovered and raised
        # in the main threads (a common error is mismatching the HMM and the
        # sequence alphabets).
        rng = pyhmmer.easel.Randomness(42)
        seqs = [TextSequence().digitize(Alphabet.dna())]
        hmm = HMM.sample(Alphabet.amino(), 100, rng)
        self.assertRaises(ValueError, self.get_hits, hmm, seqs)

    def test_no_queries(self):
        seqs = self._random_sequences()
        hits = pyhmmer.hmmsearch([], seqs, parallel=self.parallel)
        self.assertIs(None, next(hits, None))


class TestHmmsearchSingle(TestHmmsearch, unittest.TestCase):
    def get_hits(self, hmm, seqs):
        return list(pyhmmer.hmmsearch(hmm, seqs, cpus=1, parallel=self.parallel))[0]

    def get_hits_multi(self, hmm, seqs):
        return list(pyhmmer.hmmsearch(hmm, seqs, cpus=1, parallel=self.parallel))

    def test_no_queries(self):
        seqs = self._random_sequences()
        hits = pyhmmer.hmmsearch([], seqs, cpus=1, parallel=self.parallel)
        self.assertIs(None, next(hits, None))

@unittest.skipIf(platform.system() == "Darwin", "may deadlock on MacOS")
class TestHmmsearchProcess(TestHmmsearch, unittest.TestCase):
    def get_hits(self, hmm, seqs):
        return list(pyhmmer.hmmsearch(hmm, seqs, cpus=2, backend="multiprocessing", parallel=self.parallel))[0]

    def get_hits_multi(self, hmm, seqs):
        return list(pyhmmer.hmmsearch(hmm, seqs, cpus=2, backend="multiprocessing", parallel=self.parallel))

    def test_no_queries(self):
        seqs = self._random_sequences()
        hits = pyhmmer.hmmsearch([], seqs, cpus=2, backend="multiprocessing", parallel=self.parallel)
        self.assertIs(None, next(hits, None))


class TestHmmsearchReverse(TestHmmsearch):
    parallel = "targets"

    @unittest.skip("unsupported parallel mode with sequence file")
    def test_pf02826_file(self):
        pass


class TestHmmsearchReverseSingle(TestHmmsearchSingle):
    parallel = "targets"

    @unittest.skip("unsupported parallel mode with sequence file")
    def test_pf02826_file(self):
        pass


@unittest.skipIf(platform.system() == "Darwin", "may deadlock on MacOS")
class TestHmmsearchReverseProcess(TestHmmsearchProcess):
    parallel = "targets"

    @unittest.skip("unsupported parallel mode with sequence file")
    def test_pf02826_file(self):
        pass


class TestPipelinesearch(_TestSearch, unittest.TestCase):
    def get_hits(self, hmm, seqs):
        pipeline = Pipeline(alphabet=hmm.alphabet)
        hits = pipeline.search_hmm(hmm, seqs)
        return hits


class TestHmmpress(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.NamedTemporaryFile(suffix=".hmm", delete=False).name

    def tearDown(self):
        for ext in ["", ".h3m", ".h3p", ".h3i", ".h3f"]:
            if os.path.exists(self.tmp + ext):
                os.remove(self.tmp + ext)

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_roundtrip(self):
        db_folder = resource_files(__package__).joinpath("data", "hmms", "db")
        self.hmm = db_folder.joinpath("Thioesterase.hmm")
        self.h3p = db_folder.joinpath("Thioesterase.hmm.h3p")
        self.h3m = db_folder.joinpath("Thioesterase.hmm.h3m")
        self.h3f = db_folder.joinpath("Thioesterase.hmm.h3f")
        self.h3i = db_folder.joinpath("Thioesterase.hmm.h3f")
        with HMMFile(self.hmm) as hmms:
            n = pyhmmer.hmmer.hmmpress(hmms, self.tmp)
            self.assertEqual(n, 1)
        with HMMFile(self.tmp) as hmm_file:
            hmm = next(hmm_file)
            self.assertEqual(hmm.name, b"Thioesterase")


class TestPhmmer(unittest.TestCase):

    @staticmethod
    def _random_sequences(n=20):
        rng = pyhmmer.easel.Randomness(42)
        alphabet = Alphabet.amino()
        return pyhmmer.easel.DigitalSequenceBlock(
            alphabet,
            [
                pyhmmer.easel.DigitalSequence.sample(alphabet, 200, rng)
                for _ in range(10)
            ]
        )

    @staticmethod
    def table(name):
        path = resource_files(__package__).joinpath("data", "tables", name)
        return path.open()

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_callback_error(self):

        class MyException(Exception):
            pass

        def callback(hmm, total):
            raise MyException("oopsie")

        alphabet = Alphabet.amino()
        path = resource_files(__package__).joinpath("data", "seqs", "PKSI.faa")
        with SequenceFile(path, digital=True, alphabet=alphabet) as seqs_file:
            seqs = seqs_file.read_block()

        hits = pyhmmer.phmmer(seqs[-1:], seqs, cpus=1, callback=callback)
        with self.assertRaises(MyException):
            hit = next(hits)

        hits = pyhmmer.phmmer(seqs[-1:], seqs, cpus=2, callback=callback)
        with self.assertRaises(MyException):
            hit = next(hits)

    def test_no_queries(self):
        seqs = self._random_sequences()
        hits = pyhmmer.phmmer([], seqs, cpus=1)
        self.assertIs(None, next(hits, None))

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_pksi(self):
        alphabet = Alphabet.amino()
        path = resource_files(__package__).joinpath("data", "seqs", "PKSI.faa")
        with SequenceFile(path, digital=True, alphabet=alphabet) as seqs_file:
            seqs = seqs_file.read_block()
        hits = next(pyhmmer.phmmer(seqs[-1:], seqs, cpus=1))
        hits.sort()

        with self.table("A0A089QRB9.domtbl") as table:
            lines = iter(filter(lambda line: not line.startswith("#"), table))
            it = ((hit, domain) for hit in hits for domain in hit.domains)
            for line, (hit, domain) in itertools.zip_longest(lines, it):
                self.assertIsNot(line, None)
                self.assertIsNot(hit, None)
                fields = list(filter(None, line.strip().split(" ")))

                self.assertEqual(hit.name.decode(), fields[0])
                self.assertAlmostEqual(hit.score, float(fields[7]), delta=0.1)
                self.assertAlmostEqual(hit.bias, float(fields[8]), delta=0.1)
                self.assertAlmostEqual(hit.evalue, float(fields[6]), delta=0.1)

                self.assertAlmostEqual(domain.i_evalue, float(fields[12]), delta=0.1)
                self.assertAlmostEqual(domain.score, float(fields[13]), delta=0.1)

                self.assertEqual(domain.alignment.hmm_from, int(fields[15]))
                self.assertEqual(domain.alignment.hmm_to, int(fields[16]))
                self.assertEqual(domain.alignment.target_from, int(fields[17]))
                self.assertEqual(domain.alignment.target_to, int(fields[18]))
                self.assertEqual(domain.env_from, int(fields[19]))
                self.assertEqual(domain.env_to, int(fields[20]))


@unittest.skipUnless(resource_files, "importlib.resources not available")
class TestJackhmmer(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        with cls.seqs_file("PKSI", digital=True) as seqs_file:
            cls.pksi = seqs_file.read_block()

    @staticmethod
    def table(name):
        path = resource_files(__package__).joinpath("data", "tables", name)
        return path.open()

    @staticmethod
    def seqs_file(name, digital=False):
        seqs_path = resource_files(__package__).joinpath("data", "seqs", "{}.faa".format(name))
        return SequenceFile(seqs_path, digital=digital)

    @staticmethod
    def hmm_file(name):
        path = resource_files(__package__).joinpath("data", "hmms", "txt", "{}.hmm".format(name))
        return HMMFile(path)

    def test_callback_error(self):

        class MyException(Exception):
            pass

        def callback(hmm, total):
            raise MyException("oopsie")

        seqs = self.pksi
        results = pyhmmer.jackhmmer(seqs[-1:], seqs, cpus=1, max_iterations=1, callback=callback)
        with self.assertRaises(MyException):
            result = next(results)

        results = pyhmmer.jackhmmer(seqs[-1:], seqs, cpus=2, max_iterations=1, callback=callback)
        with self.assertRaises(MyException):
            result = next(results)

    def test_no_queries(self):
        seqs = self.pksi
        result = pyhmmer.jackhmmer([], seqs, cpus=1, max_iterations=1)
        self.assertIs(None, next(result, None))

    def test_pksi(self):
        seqs = self.pksi
        results = pyhmmer.jackhmmer(seqs[-1:], seqs, cpus=1, max_iterations=1)
        result = next(results)
        self.assertEqual(result.iteration, 1)
        result.hits.sort()

        with self.table("A0A089QRB9.domtbl") as table:
            lines = iter(filter(lambda line: not line.startswith("#"), table))
            it = ((hit, domain) for hit in result.hits for domain in hit.domains)
            for line, (hit, domain) in itertools.zip_longest(lines, it):
                self.assertIsNot(line, None)
                self.assertIsNot(hit, None)
                fields = list(filter(None, line.strip().split(" ")))

                self.assertEqual(hit.name.decode(), fields[0])
                self.assertAlmostEqual(hit.score, float(fields[7]), delta=0.1)
                self.assertAlmostEqual(hit.bias, float(fields[8]), delta=0.1)
                self.assertAlmostEqual(hit.evalue, float(fields[6]), delta=0.1)

                self.assertAlmostEqual(domain.i_evalue, float(fields[12]), delta=0.1)
                self.assertAlmostEqual(domain.score, float(fields[13]), delta=0.1)

                self.assertEqual(domain.alignment.hmm_from, int(fields[15]))
                self.assertEqual(domain.alignment.hmm_to, int(fields[16]))
                self.assertEqual(domain.alignment.target_from, int(fields[17]))
                self.assertEqual(domain.alignment.target_to, int(fields[18]))
                self.assertEqual(domain.env_from, int(fields[19]))
                self.assertEqual(domain.env_to, int(fields[20]))

    def test_pksi_checkpoint(self):
        seqs = self.pksi
        # jackhmmer CLI converges in 3 iterations, 5 hits, 17 sequences in MSA
        iterations = next(
            pyhmmer.jackhmmer(
                seqs[-1],
                seqs,
                cpus=1,
                checkpoints=True,
            )
        )
        self.assertEqual(len(iterations), 3)
        self.assertTrue(iterations[-1].converged)
        self.assertEqual(len(iterations[-1].hits), 5)
        self.assertEqual(len(iterations[-1].msa.sequences), 17)

    def test_thioestherase(self):
        with self.hmm_file("Thioesterase") as hmm_file:
            hmm = hmm_file.read()
        with self.seqs_file("938293.PRJEB85.HG003687", digital=True) as seqs_file:
            seqs = seqs_file.read_block()

        result = next(
            pyhmmer.jackhmmer(
                hmm, seqs, cpus=1, max_iterations=1, incE=0.1, incdomE=0.1
            )
        )
        # unpack IterationResult
        _, hits, _, _, it = result
        self.assertEqual(it, 1)
        self.assertEqual(len(hits), 1)

        hits.sort()

        hit = hits[0]
        self.assertEqual(hit.name, b"938293.PRJEB85.HG003687_113")
        self.assertAlmostEqual(hit.score, 8.6, delta=0.1)  # printed with %6.1f
        self.assertAlmostEqual(hit.bias, 1.5, delta=0.1)  # printed with  %5.1f
        self.assertAlmostEqual(hit.evalue, 0.096, delta=0.01)  # printed with %9.2g
        self.assertEqual(len(hit.domains), 1)

        domain = hit.domains[0]
        self.assertAlmostEqual(domain.score, 8.1, delta=0.1)
        self.assertAlmostEqual(domain.bias, 1.5, delta=0.1)
        self.assertAlmostEqual(domain.i_evalue, 0.14, places=2)  # printed with %9.2g
        self.assertAlmostEqual(domain.c_evalue, 6.5e-05, places=2)  # printed with %9.2g
        self.assertEqual(domain.alignment.target_from, 115)
        self.assertEqual(domain.alignment.target_to, 129)
        self.assertEqual(domain.alignment.hmm_from, 79)
        self.assertEqual(domain.alignment.hmm_to, 93)
        self.assertEqual(domain.env_from, 115)
        self.assertEqual(domain.env_to, 129)

        last = hit.domains[-1]
        self.assertEqual(domain.alignment.hmm_name, last.alignment.hmm_name)


@unittest.skipUnless(resource_files, "importlib.resources not available")
class TestNhmmer(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        path = resource_files(__package__).joinpath("data", "hmms", "txt", "bmyD.hmm")
        with HMMFile(path) as hmm_file:
            cls.bmyD = next(hmm_file)
        path = resource_files(__package__).joinpath("data", "hmms", "txt", "RF00001.hmm")
        with HMMFile(path) as hmm_file:
            cls.rf00001 = next(hmm_file)

    @staticmethod
    def table(name):
        path = resource_files(__package__).joinpath("data", "tables", name)
        return path.open()

    def assertTableEqual(self, hits, table):
        lines = iter(filter(lambda line: not line.startswith("#"), table))
        reported_hits = filter(lambda hit: hit.reported, hits)
        for line, hit in itertools.zip_longest(lines, reported_hits):
            self.assertIsNot(line, None)
            self.assertIsNot(hit, None)
            fields = list(filter(None, line.strip().split(" ")))
            self.assertEqual(hit.name.decode(), fields[0])
            if fields[1] == "-":
                self.assertIs(hit.accession, None)
            else:
                self.assertEqual(hit.accession.decode(), fields[1])
            self.assertAlmostEqual(hit.best_domain.bias, float(fields[14]), delta=0.1)
            self.assertAlmostEqual(hit.best_domain.score, float(fields[13]), delta=0.1)
            self.assertAlmostEqual(
                hit.best_domain.i_evalue, float(fields[12]), delta=0.1
            )

    def test_no_queries(self):
        alphabet = Alphabet.dna()
        path = resource_files(__package__).joinpath("data", "seqs", "BGC0001090.gbk")
        with SequenceFile(path, digital=True, alphabet=alphabet) as seqs_file:
            seqs = seqs_file.read_block()
        hits = pyhmmer.nhmmer([], seqs, cpus=1)
        self.assertIs(None, next(hits, None))

    def test_bmyd_seq_bgc_block(self):
        alphabet = Alphabet.dna()

        path = resource_files(__package__).joinpath("data", "seqs", "bmyD.fna")
        with SequenceFile(path, digital=True, alphabet=alphabet) as seqs_file:
            query = next(seqs_file)

        path = resource_files(__package__).joinpath("data", "seqs", "BGC0001090.gbk")
        with SequenceFile(
            path, "genbank", digital=True, alphabet=alphabet
        ) as seqs_file:
            seqs = seqs_file.read_block()

        hits = next(pyhmmer.nhmmer(query, seqs, cpus=1))
        hits.sort()

        self.assertEqual(len(hits), 1)
        with self.table("bmyD3.tbl") as table:
            self.assertTableEqual(hits, table)

    def test_bmyd_seq_bgc_file(self):
        alphabet = Alphabet.dna()

        path = resource_files(__package__).joinpath("data", "seqs", "bmyD.fna")
        with SequenceFile(path, digital=True, alphabet=alphabet) as seqs_file:
            query = next(seqs_file)

        path = resource_files(__package__).joinpath("data", "seqs", "BGC0001090.gbk")
        with SequenceFile(path, "genbank", digital=True, alphabet=alphabet) as seqs:
            hits = list(pyhmmer.nhmmer(query, seqs, cpus=1))[0]
            hits.sort()

        self.assertEqual(len(hits.reported), 1)
        with self.table("bmyD3.tbl") as table:
            self.assertTableEqual(hits, table)

    def test_bmyd_msa_bgc_block(self):
        alphabet = Alphabet.dna()

        path = resource_files(__package__).joinpath("data", "seqs", "bmyD.fna")
        with SequenceFile(path, digital=True, alphabet=alphabet) as seqs_file:
            query = DigitalMSA(alphabet, name=b"bmyD", sequences=[next(seqs_file)])

        path = resource_files(__package__).joinpath("data", "seqs", "BGC0001090.gbk")
        with SequenceFile(
            path, "genbank", digital=True, alphabet=alphabet
        ) as seqs_file:
            seqs = seqs_file.read_block()

        hits = next(pyhmmer.nhmmer(query, seqs, cpus=1))
        self.assertEqual(len(hits), 1)

    def test_bmyd_msa_bgc_file(self):
        alphabet = Alphabet.dna()

        path = resource_files(__package__).joinpath("data", "seqs", "bmyD.fna")
        with SequenceFile(path, digital=True, alphabet=alphabet) as seqs_file:
            query = DigitalMSA(alphabet, name=b"bmyD", sequences=[next(seqs_file)])

        path = resource_files(__package__).joinpath("data", "seqs", "BGC0001090.gbk")
        with SequenceFile(path, "genbank", digital=True, alphabet=alphabet) as seqs:
            hits = list(pyhmmer.nhmmer(query, seqs, cpus=1))[0]
            self.assertEqual(len(hits.reported), 1)

    def test_bmyd_hmm_bgc_block(self):
        alphabet = Alphabet.dna()

        path = resource_files(__package__).joinpath("data", "seqs", "BGC0001090.gbk")
        with SequenceFile(
            path, "genbank", digital=True, alphabet=alphabet
        ) as seqs_file:
            seqs = seqs_file.read_block()

        hits = next(pyhmmer.nhmmer(self.bmyD, seqs, cpus=1))
        hits.sort()

        self.assertEqual(len(hits.reported), 2)
        with self.table("bmyD1.tbl") as table:
            self.assertTableEqual(hits, table)

    def test_bmyd_hmm_bgc_file(self):
        alphabet = Alphabet.dna()

        path = resource_files(__package__).joinpath("data", "seqs", "BGC0001090.gbk")
        with SequenceFile(
            path, "genbank", digital=True, alphabet=alphabet
        ) as seqs_file:
            hits = list(pyhmmer.nhmmer(self.bmyD, seqs_file, cpus=1))[0]
            hits.sort()

        self.assertEqual(len(hits.reported), 2)
        with self.table("bmyD1.tbl") as table:
            self.assertTableEqual(hits, table)

    def test_bmyd_hmm_genome_block(self):
        alphabet = Alphabet.dna()

        path = resource_files(__package__).joinpath("data", "seqs", "1390.SAMEA104415756.OFHT01000022.fna")
        with SequenceFile(path, "fasta", digital=True, alphabet=alphabet) as seqs_file:
            seqs = seqs_file.read_block()

        hits = next(pyhmmer.nhmmer(self.bmyD, seqs, cpus=1))
        hits.sort()

        self.assertEqual(len(hits.reported), 3)
        self.assertEqual(len(hits.included), 2)
        with self.table("bmyD2.tbl") as table:
            self.assertTableEqual(hits, table)

    def test_bmyd_hmm_genome_file(self):
        alphabet = Alphabet.dna()

        path = resource_files(__package__).joinpath("data", "seqs", "1390.SAMEA104415756.OFHT01000022.fna")
        with SequenceFile(path, "fasta", digital=True, alphabet=alphabet) as seqs_file:
            hits = list(pyhmmer.nhmmer(self.bmyD, seqs_file, cpus=1))[0]
            hits.sort()

        self.assertEqual(len(hits.reported), 3)
        self.assertEqual(len(hits.included), 2)
        with self.table("bmyD2.tbl") as table:
            self.assertTableEqual(hits, table)

    def test_rf0001_genome_file(self):
        alphabet = Alphabet.rna()
        path = resource_files(__package__).joinpath("data", "seqs", "1390.SAMEA104415756.OFHT01000024.fna")
        with SequenceFile(path, "fasta", digital=True, alphabet=alphabet) as seqs_file:
            hits = list(pyhmmer.nhmmer(self.rf00001, seqs_file, cpus=1))[0]
            hits.sort()

        self.assertEqual(len(hits), 1)
        self.assertAlmostEqual(hits[0].evalue, 2.5e-17, places=2)
        self.assertEqual(hits[0].best_domain.strand, "-")

    def test_rf0001_genome_file_wlen_3878(self):
        alphabet = Alphabet.rna()
        path = resource_files(__package__).joinpath("data", "seqs", "1390.SAMEA104415756.OFHT01000024.fna")
        with SequenceFile(path, "fasta", digital=True, alphabet=alphabet) as seqs_file:
            hits = list(pyhmmer.nhmmer(self.rf00001, seqs_file, cpus=1, window_length=3878))[0]
            hits.sort()

        self.assertEqual(len(hits), 2)
        self.assertAlmostEqual(hits[0].evalue, 5.4e-17, places=2)
        self.assertAlmostEqual(hits[1].evalue, 0.3, places=2)
        self.assertEqual(hits[0].best_domain.strand, "-")
        self.assertEqual(hits[1].best_domain.strand, "-")


class TestHmmalign(unittest.TestCase):
    def setUp(self):
        self.tmpout = tempfile.NamedTemporaryFile(suffix=".hmm", delete=False).name

    def tearDown(self):
        os.remove(self.tmpout)

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_luxc(self):
        hmm_path = resource_files(__package__).joinpath("data", "hmms", "txt", "LuxC.hmm")
        seqs_path = resource_files(__package__).joinpath("data", "seqs", "LuxC.faa")
        ref_path = resource_files(__package__).joinpath("data", "msa", "LuxC.hmmalign.sto")

        with HMMFile(hmm_path) as hmm_file:
            hmm = hmm_file.read()
        with SequenceFile(seqs_path, digital=True, alphabet=hmm.alphabet) as seqs_file:
            seqs = seqs_file.read_block()
        with MSAFile(ref_path) as ref_file:
            ref = ref_file.read()

        msa = pyhmmer.hmmalign(hmm, seqs, trim=True, cpus=1)
        self.assertEqual(msa, ref)


class TestHMMScan(unittest.TestCase):
    @staticmethod
    def table(name):
        path = resource_files(__package__).joinpath("data", "tables", name)
        return path.open()

    @staticmethod
    def hmm_file(name):
        path = resource_files(__package__).joinpath("data", "hmms", "db", "{}.hmm".format(name))
        return HMMFile(path)

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_rrefam_block(self):
        # get paths to resources
        table_path = resource_files(__package__).joinpath("data", "tables", "RREFam.scan.tbl")
        db_path = resource_files(__package__).joinpath("data", "hmms", "db", "RREFam.hmm")
        seqs_path = resource_files(__package__).joinpath("data", "seqs", "PKSI.faa")

        # load expected results from the hmmscan table
        expected = {}
        with open(table_path) as table:
            lines = filter(lambda line: not line.startswith("#"), table)
            for query_name, query_lines in itertools.groupby(
                lines, key=lambda line: line.strip().split()[2]
            ):
                expected[query_name] = list(query_lines)

        # pre-load the profile database so that `hmmscan` will create a profile block
        with HMMFile(db_path) as hmm_file:
            hmms = list(hmm_file)

        # scan with the sequences and check the hits are equal
        with SequenceFile(
            seqs_path, digital=True, alphabet=hmms[0].alphabet
        ) as seqs_file:
            for hits in pyhmmer.hmmer.hmmscan(seqs_file, hmms, cpus=1):
                expected_lines = expected.get(hits.query.name.decode())
                if expected_lines is None:
                    self.assertEqual(len(hits), 0)
                    continue
                for line, hit in itertools.zip_longest(expected_lines, hits):
                    self.assertIsNot(line, None)
                    self.assertIsNot(hit, None)
                    fields = list(filter(None, line.strip().split(" ")))
                    self.assertEqual(hit.name.decode(), fields[0])
                    self.assertAlmostEqual(hit.score, float(fields[5]), delta=0.1)
                    self.assertAlmostEqual(hit.bias, float(fields[6]), delta=0.1)
                    self.assertAlmostEqual(hit.evalue, float(fields[4]), delta=0.1)

    @unittest.skipUnless(resource_files, "importlib.resources not available")
    def test_rrefam_file(self):
        # get paths to resources
        table_path = resource_files(__package__).joinpath("data", "tables", "RREFam.scan.tbl")
        db_path = resource_files(__package__).joinpath("data", "hmms", "db", "RREFam.hmm")
        seqs_path = resource_files(__package__).joinpath("data", "seqs", "PKSI.faa")

        # load expected results from the hmmscan table
        expected = {}
        with open(table_path) as table:
            lines = filter(lambda line: not line.startswith("#"), table)
            for query_name, query_lines in itertools.groupby(
                lines, key=lambda line: line.strip().split()[2]
            ):
                expected[query_name] = list(query_lines)

        # scan with the sequences and check the hits are equal, using the file
        # to read the profiles from (the files will be rewinded before each query)
        with SequenceFile(seqs_path, digital=True) as seqs_file:
            with HMMPressedFile(db_path) as pressed_file:
                for hits in pyhmmer.hmmer.hmmscan(seqs_file, pressed_file, cpus=1):
                    expected_lines = expected.get(hits.query.name.decode())
                    if expected_lines is None:
                        self.assertEqual(len(hits), 0)
                        continue
                    for line, hit in itertools.zip_longest(expected_lines, hits):
                        self.assertIsNot(line, None)
                        self.assertIsNot(hit, None)
                        fields = list(filter(None, line.strip().split(" ")))
                        self.assertEqual(hit.name.decode(), fields[0])
                        self.assertAlmostEqual(hit.score, float(fields[5]), delta=0.1)
                        self.assertAlmostEqual(hit.bias, float(fields[6]), delta=0.1)
                        self.assertAlmostEqual(hit.evalue, float(fields[4]), delta=0.1)
