"""Pruebas de crc.c y de las afirmaciones de los apuntes sobre el CRC."""
import itertools
import subprocess
import unittest
import zlib

CRC = "build/crc"


def run_crc(*args):
    return subprocess.run([CRC, *args], text=True, capture_output=True, check=False)


def remainder(word: str, gen: str) -> str:
    """División módulo 2 hecha aparte, en Python, con enteros."""
    value, divisor, r = int(word, 2), int(gen, 2), len(gen) - 1
    for shift in range(len(word) - len(gen), -1, -1):
        if value >> (shift + r) & 1:
            value ^= divisor << shift
    return format(value, f"0{r}b")


def flip(word: str, error: int) -> str:
    return format(int(word, 2) ^ error, f"0{len(word)}b")


class CrcProgram(unittest.TestCase):
    def test_worked_example_of_the_notes(self):
        out = run_crc("1101011", "1001").stdout
        self.assertIn("CRC (r = 3 bits): 111", out)
        self.assertIn("se transmite: 1101011111", out)

    def test_received_word_without_errors(self):
        done = run_crc("-c", "1101011111", "1001")
        self.assertEqual(done.returncode, 0)
        self.assertIn("residuo: 000", done.stdout)

    def test_single_bit_error_is_detected(self):
        done = run_crc("-c", "1101011011", "1001")
        self.assertEqual(done.returncode, 1)
        self.assertIn("ERROR detectado", done.stdout)

    def test_error_equal_to_generator_goes_unnoticed(self):
        # El error 1001, dos bits a distancia 3, es múltiplo del generador 1001.
        bad = flip("1101011111", 0b1001)
        self.assertEqual(run_crc("-c", bad, "1001").returncode, 0)
        # El ejemplo de los apuntes: el mismo patrón, desplazado tres posiciones.
        self.assertEqual(flip("1101011111", 0b1001 << 3), "1100010111")
        self.assertEqual(run_crc("-c", "1100010111", "1001").returncode, 0)

    def test_matches_independent_division(self):
        for bits in range(1, 8):
            for data in map("".join, itertools.product("01", repeat=bits)):
                for gen in ("1001", "1011", "10011"):
                    expected = remainder(data + "0" * (len(gen) - 1), gen)
                    self.assertIn(f"bits): {expected}\n", run_crc(data, gen).stdout)

    def test_rejects_bad_arguments(self):
        for args in (["10a", "1001"], ["101", "0110"], ["101", "1"], ["101"]):
            self.assertEqual(run_crc(*args).returncode, 2, args)


class CrcClaims(unittest.TestCase):
    WORD = "1101011111"

    def test_every_burst_up_to_r_bits_is_detected(self):
        # Una ráfaga de largo b <= r empieza y termina en 1: con r = 3 son 1, 11, 101 y 111.
        for pattern in (0b1, 0b11, 0b101, 0b111):
            for shift in range(len(self.WORD) - pattern.bit_length() + 1):
                bad = flip(self.WORD, pattern << shift)
                self.assertNotEqual(remainder(bad, "1001"), "000")

    def test_generator_1001_detects_every_odd_error(self):
        # 1001 tiene a x + 1 como factor: una cantidad impar de errores nunca da residuo 0.
        n = len(self.WORD)
        for error in range(1, 2 ** n):
            if bin(error).count("1") % 2 == 1:
                self.assertNotEqual(remainder(flip(self.WORD, error), "1001"), "000")

    def test_fraction_of_undetected_errors_is_one_in_eight(self):
        # Con r = 3, de todos los patrones de error no nulos, uno de cada 8 pasa (aprox.).
        n = len(self.WORD)
        missed = sum(remainder(flip(self.WORD, e), "1001") == "000" for e in range(1, 2 ** n))
        self.assertEqual(missed, 2 ** (n - 3) - 1)

    def test_ethernet_crc32_check_value(self):
        # zlib usa el mismo polinomio y las mismas reflexiones que el FCS de Ethernet.
        self.assertEqual(zlib.crc32(b"123456789"), 0xCBF43926)


if __name__ == "__main__":
    unittest.main()
