import abc
import copy
import gc
import io
import os
import platform
import pickle
import unittest
import time
import tempfile
import warnings

from pyhmmer import easel
from pyhmmer.errors import AlphabetMismatch


class _TestSequenceBase(abc.ABC):

    @classmethod
    @abc.abstractmethod
    def Sequence(cls, **kwargs):
        pass

    def test_init_empty(self):
        seq = self.Sequence()
        self.assertEqual(seq.name, "")
        self.assertEqual(len(seq), 0)
        self.assertEqual(len(seq.sequence), 0)

    def test_setter_name(self):
        seq = self.Sequence()
        self.assertEqual(seq.name, "")
        seq.name = "OTHER"
        self.assertEqual(seq.name, "OTHER")

    def test_setter_description(self):
        seq = self.Sequence()
        self.assertIs(seq.description, "")
        seq.description = "a test sequence"
        self.assertEqual(seq.description, "a test sequence")

    def test_setter_residue_markup(self):
        seq = self.Sequence(sequence="ATTGC")
        self.assertEqual(seq.residue_markups, {})
        seq.residue_markups = {"kind": "DADDC"}
        self.assertEqual(seq.residue_markups["kind"], "DADDC")
        with self.assertRaises(ValueError):
            seq.residue_markups = {"kind": "D"}

    def test_pickle(self):
        s1 = self.Sequence(name="test1", sequence="GAATTC")
        s1a = pickle.loads(pickle.dumps(s1))
        self.assertEqual(s1.name, s1a.name)
        self.assertEqual(s1.sequence, s1a.sequence)
        self.assertEqual(s1.description, "")
        self.assertEqual(s1.residue_markups, {})

    def test_pickle_residue_markups(self):
        s2 = self.Sequence(
            name="test2", 
            sequence="TTCAAC", 
            residue_markups={"structure": "((.))."}
        )
        s2a = pickle.loads(pickle.dumps(s2))
        self.assertEqual(s2.name, s2a.name)
        self.assertEqual(s2.sequence, s2a.sequence)
        self.assertEqual(s2.residue_markups, s2a.residue_markups)

    def test_copy(self):
        seq = self.Sequence(name="TEST", sequence="ATGC")

        # copy the sequence
        cpy = seq.copy()

        # check the copy is equal and attributes are equal
        self.assertEqual(seq, cpy)
        self.assertEqual(seq.name, cpy.name)
        self.assertEqual(seq.checksum(), cpy.checksum())

        # check attributes can be read even after the original object
        # is (hopefully) deallocated, to make sure internal strings were copied
        del seq
        gc.collect()
        self.assertEqual(cpy.name, "TEST")

        # check `copy.copy` works too
        cpy2 = copy.copy(cpy)
        self.assertEqual(cpy, cpy2)
        self.assertEqual(cpy.name, cpy2.name)

    def test_eq(self):
        seq1 = self.Sequence(name="TEST", sequence="ATGC")
        self.assertEqual(seq1, seq1)
        self.assertNotEqual(seq1, object())

        seq2 = self.Sequence(name="TEST", sequence="ATGC")
        self.assertEqual(seq1, seq2)

        seq3 = self.Sequence(name="OTHER", sequence="ATGC")
        self.assertNotEqual(seq1, seq3)

        seq4 = self.Sequence(name="TEST", sequence="ATGT")
        self.assertNotEqual(seq1, seq4)

    def test_eq_not_seq(self):
        seq1 = self.Sequence(name="TEST", sequence="ATGC")
        self.assertNotEqual(seq1, 1)
        self.assertNotEqual(seq1, "hello")


class TestSequence(unittest.TestCase):

    def test_init_abstract(self):
        self.assertRaises(TypeError, easel.Sequence, name="ecoRI", sequence="GAATTC")

    def test_write_roundtrip(self):
        seq = easel.TextSequence(name="ecoRI", sequence="GAATTC")
        with io.BytesIO() as buffer:
            seq.write(buffer)
            seq2 = easel.SequenceFile.parse(buffer.getvalue(), "fasta")
        self.assertEqual(seq, seq2)


class TestDigitalSequence(_TestSequenceBase, unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.abc = easel.Alphabet.dna()

    @classmethod
    def Sequence(cls, **kwargs):
        seq = kwargs.get("sequence")
        if seq:
            kwargs["sequence"] = cls.abc.encode(seq)
        return easel.DigitalSequence(cls.abc, **kwargs)

    def test_sample(self):
        seq = easel.DigitalSequence.sample(self.abc, 10)
        self.assertLessEqual(len(seq), 10)
        self.assertIs(seq.alphabet, self.abc)

    def test_sample_seed(self):
        seq1 = easel.DigitalSequence.sample(self.abc, 10, randomness=easel.Randomness(42))
        seq2 = easel.DigitalSequence.sample(self.abc, 10, randomness=42)
        self.assertEqual(seq1, seq2)
        seq3 = easel.DigitalSequence.sample(self.abc, 10, randomness=100)
        self.assertNotEqual(seq1, seq3)

        with self.assertRaises(TypeError):
            seq4 = easel.DigitalSequence.sample(self.abc, 10, randomness="abc")

    def test_init_kwargs(self):
        arr = bytearray([0, 1, 2, 3])
        seq = easel.DigitalSequence(self.abc, name="TEST", sequence=arr)
        self.assertEqual(seq.name, "TEST")
        self.assertEqual(bytearray(seq.sequence), arr)
        self.assertEqual(len(seq), 4)

    def test_textize_roundtrip(self):
        arr = bytearray([0, 1, 2, 3])
        dsq1 = easel.DigitalSequence(self.abc, name="TEST", sequence=arr)
        tsq1 = dsq1.textize()
        self.assertEqual(tsq1.name, dsq1.name)

        dsq2 = tsq1.digitize(self.abc)
        self.assertEqual(dsq1.name, dsq2.name)
        self.assertEqual(list(dsq1.sequence), list(dsq2.sequence))
        self.assertEqual(dsq1, dsq2)

    def test_reverse_complement(self):
        seq = easel.TextSequence(sequence="ATGC").digitize(self.abc)
        rc = seq.reverse_complement()
        self.assertEqual(rc.textize().sequence, "GCAT")

    def test_reverse_complement_inplace(self):
        seq = easel.TextSequence(sequence="ATGC").digitize(self.abc)
        seq.reverse_complement(inplace=True)
        self.assertEqual(seq.textize().sequence, "GCAT")

    def test_reverse_complement_protein(self):
        abc = easel.Alphabet.amino()
        seq = easel.TextSequence(sequence="MEMLP").digitize(abc)
        self.assertRaises(ValueError, seq.reverse_complement)
        self.assertRaises(ValueError, seq.reverse_complement, inplace=True)

    def test_invalid_characters(self):
        self.assertRaises(ValueError, easel.DigitalSequence, self.abc, name="TEST", sequence=b"?!\xf1")

    def test_memoryview(self):
        seq = easel.TextSequence(sequence="ATGC").digitize(self.abc)
        mem = memoryview(seq)
        self.assertEqual(mem.tobytes(), b"\0\3\2\1")

    def test_translate(self):
        gencode = easel.GeneticCode()
        seq = easel.TextSequence(sequence="ATGCTGCCCGGTTTGGCACTGCTCCTGCTGGCCGCC").digitize(gencode.nucleotide_alphabet)
        prot = seq.translate(gencode).textize()
        self.assertEqual(prot.sequence, "MLPGLALLLLAA")

    def test_translate_copy_metadata(self):
        gencode = easel.GeneticCode()
        textseq = easel.TextSequence(sequence="ATGCTGCCCGGT", name="seq", source="source")
        seq = textseq.digitize(gencode.nucleotide_alphabet)
        prot = seq.translate(gencode).textize()
        self.assertEqual(prot.name, textseq.name)      
        self.assertEqual(prot.source, textseq.source)      

    def test_translate_empty(self):
        gencode = easel.GeneticCode()
        seq = easel.DigitalSequence(gencode.nucleotide_alphabet)
        prot = seq.translate(gencode).textize()
        self.assertEqual(prot.sequence, "")

    def test_translate_alphabet_mismatch(self):
        gencode = easel.GeneticCode()
        with self.assertRaises(AlphabetMismatch):
            prot = easel.DigitalSequence(gencode.amino_alphabet).translate(gencode)


class TestTextSequence(_TestSequenceBase, unittest.TestCase):

    @classmethod
    def Sequence(cls, **kwargs):
        return easel.TextSequence(**kwargs)

    def test_sample(self):
        seq = easel.TextSequence.sample(10)
        self.assertLessEqual(len(seq), 10)

    def test_sample_seed(self):
        seq1 = easel.TextSequence.sample(10, randomness=easel.Randomness(42))
        seq2 = easel.TextSequence.sample(10, randomness=42)
        self.assertEqual(seq1, seq2)
        seq3 = easel.TextSequence.sample(10, randomness=100)
        self.assertNotEqual(seq1, seq3)

        with self.assertRaises(TypeError):
            seq4 = easel.TextSequence.sample(10, randomness="abc")

    def test_init_kwargs(self):
        seq = easel.TextSequence(name="TEST", sequence="ATGC")
        self.assertEqual(seq.name, "TEST")
        self.assertEqual(seq.sequence, "ATGC")
        self.assertEqual(len(seq), 4)

    def test_digitize_roundtrip(self):
        seq1 = easel.TextSequence(name="TEST", sequence="ATGC")
        dsq1 = seq1.digitize(easel.Alphabet.dna())
        self.assertEqual(seq1.name, dsq1.name)

        seq2 = dsq1.textize()
        self.assertEqual(seq1.name, seq2.name)
        self.assertEqual(seq1, seq2)

    def test_digitize_invalid(self):
        seq1 = easel.TextSequence(name="TEST", sequence=">v3ry 1nv4l1d")
        self.assertRaises(ValueError, seq1.digitize, easel.Alphabet.dna())
        self.assertRaises(ValueError, seq1.digitize, easel.Alphabet.amino())

    def test_memoryview(self):
        seq = self.Sequence(sequence="ATGC")
        mem = memoryview(seq)
        self.assertEqual(mem.tobytes(), b"ATGC")

    def test_reverse_complement(self):
        seq = easel.TextSequence(sequence="ATGC")
        rc = seq.reverse_complement()
        self.assertEqual(rc.sequence, "GCAT")

    def test_reverse_complement_inplace(self):
        seq = easel.TextSequence(sequence="ATGC")
        seq.reverse_complement(inplace=True)
        self.assertEqual(seq.sequence, "GCAT")

    def test_reverse_complement_protein(self):
        seq = easel.TextSequence(sequence="MEMLP")

        with warnings.catch_warnings():
            warnings.simplefilter('error')
            self.assertRaises(Warning, seq.reverse_complement)

        with warnings.catch_warnings():
            warnings.simplefilter('ignore')
            rc = seq.reverse_complement()
            self.assertEqual(rc.sequence, "NNKNK")

