"""TLS sin topología: sni_peek contra bytes malos, y la repetición de 0-RTT."""
import socket
import subprocess
import time
import unittest

import zero_rtt
from netlab import HERE, make_pki

PEEK = f"{HERE}/bin/sni_peek"


def peek_with(payloads):
    """Lanza sni_peek y le manda cada payload por su propia conexión. Devuelve (salida, código)."""
    port = zero_rtt.free_port()
    server = subprocess.Popen([PEEK, str(port), str(len(payloads))], stdout=subprocess.PIPE,
                              stderr=subprocess.STDOUT, text=True)
    try:
        time.sleep(0.4)
        for data in payloads:
            with socket.create_connection(("127.0.0.1", port), timeout=3) as sock:
                sock.sendall(data)
                sock.shutdown(socket.SHUT_WR)
                sock.recv(16)
        return server.communicate(timeout=10)[0], server.returncode
    finally:
        server.kill()
        server.wait()


def real_hello():
    """Un ClientHello real de curl, capturado con un socket propio."""
    with socket.create_server(("127.0.0.1", 0)) as listener:
        port = listener.getsockname()[1]
        curl = subprocess.Popen(["curl", "-s", "-k", "--max-time", "2", "--resolve",
                                 f"oculto.redes.test:{port}:127.0.0.1", f"https://oculto.redes.test:{port}/"])
        listener.settimeout(3)
        conn, _ = listener.accept()
        conn.settimeout(1)
        data = b""
        try:
            while True:
                piece = conn.recv(4096)
                if not piece:
                    break
                data += piece
                if len(data) >= 5 and len(data) >= 5 + int.from_bytes(data[3:5], "big"):
                    break
        finally:
            conn.close()
            curl.kill()
            curl.wait()
    return data


class SniPeek(unittest.TestCase):
    def test_reads_a_real_client_hello(self):
        output, code = peek_with([real_hello()])
        self.assertEqual(code, 0)
        self.assertIn("SNI: oculto.redes.test", output)
        self.assertIn("TLS1.3", output)

    def test_survives_every_truncation(self):
        hello = real_hello()
        cuts = [hello[:size] for size in (0, 1, 4, 5, 6, 9, 11, 43, 44, 80, 200, len(hello) - 1)]
        output, code = peek_with(cuts)
        self.assertEqual(code, 0, output)
        self.assertEqual(output.count("no es un ClientHello completo"), len(cuts), output)

    def test_lies_in_the_inner_lengths_do_not_crash_it(self):
        hello = bytearray(real_hello())
        cases = []
        for position in range(9, min(len(hello), 300), 7):
            bad = bytearray(hello)
            bad[position] = 0xff
            cases.append(bytes(bad))
        output, code = peek_with(cases)
        self.assertEqual(code, 0, output)
        self.assertEqual(output.count("conexión de"), len(cases))

    def test_control_bytes_in_the_name_do_not_reach_the_terminal(self):
        hello = bytearray(real_hello())
        at = bytes(hello).index(b"oculto")
        hello[at] = 0x1b
        output, code = peek_with([bytes(hello)])
        self.assertEqual(code, 0, output)
        self.assertIn("SNI: \\x1bculto.redes.test", output)
        self.assertNotIn("\x1b", output)

    def test_not_tls(self):
        output, code = peek_with([b"GET / HTTP/1.1\r\nHost: x\r\n\r\n"])
        self.assertEqual(code, 0)
        self.assertIn("no es un ClientHello", output)


class ZeroRttReplay(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.pki = make_pki()

    def test_replay_is_processed_twice_without_protection(self):
        heard, before, after = zero_rtt.trial(self.pki, ["-no_anti_replay"])
        self.assertIn("accepted", heard)
        self.assertEqual((before, after), (1, 2))

    def test_replay_is_rejected_with_protection(self):
        heard, before, after = zero_rtt.trial(self.pki, [])
        self.assertIn("accepted", heard)
        self.assertEqual((before, after), (1, 1))


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