"""Real loopback tests plus explicitly separate constructed-packet classification tests."""
import argparse
import hashlib
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import platform
import socket
import subprocess
import sys
import threading
import time
import urllib.request

import scapy
from scapy.all import IP, TCP, ICMP, UDP, sr1
from syn_scan import classify, configure_linux, TARGET
from packet_lab import build, capture_echo, echo

ROOT = Path(__file__).resolve().parent


def offline():
    sent = IP(src=TARGET, dst=TARGET) / TCP(sport=40000, dport=9999, seq=123, flags="S")
    def reply(flags="SA", **kw):
        fields = dict(sport=9999, dport=40000, seq=800, ack=124, flags=flags)
        fields.update(kw)
        return IP(src=TARGET, dst=TARGET) / TCP(**fields)
    cases = [("synack", reply(), "OPEN"), ("reset_ack", reply("RA"), "CLOSED"),
             ("timeout", None, "NO_RESPONSE"), ("ecn_synack", reply("SAE"), "OPEN"),
             ("conflicting_flags", reply("SAR"), "OTHER"), ("wrong_ack", reply(ack=999), "UNMATCHED"),
             ("wrong_ports", reply(dport=40001), "UNMATCHED"), ("bare_reset", reply("R"), "UNMATCHED"),
             ("icmp_quote", IP(src=TARGET,dst=TARGET)/ICMP(type=3,code=13)/sent, "ICMP"),
             ("udp", IP(src=TARGET,dst=TARGET)/UDP(), "OTHER")]
    checks = []
    for name, answer, expected in cases:
        result = classify(answer, sent)
        assert result["state"] == expected, (name, result)
        checks.append(name)
    facts = build()
    assert facts["beforeLength"] is None and facts["serializedLength"] == facts["byteLength"] == 45
    assert facts["ipChecksum"] is not None and facts["tcpChecksum"] is not None and facts["payload"] == "hello"
    assert facts["stringComparisonSupported"] is True
    return {"classificationCases": checks, "assembly": facts,
            "constructedPacketsOnly": True, "packetTransmission": False}


def run_cli(script, *args):
    proc = subprocess.run([sys.executable, str(ROOT/script), *args], capture_output=True, text=True, timeout=20)
    assert proc.returncode == 0, proc.stderr
    return json.loads(proc.stdout)


def live():
    configure_linux()
    requests = []
    class Handler(BaseHTTPRequestHandler):
        def do_GET(self):
            self.send_response(200); self.end_headers(); self.wfile.write(b"step080\n")
        def log_message(self, message, *args):
            requests.append(message % args)
    server = ThreadingHTTPServer((TARGET, 9999), Handler)
    closed = socket.socket(); closed.bind((TARGET, 0)); closed_port = closed.getsockname()[1]
    thread = threading.Thread(target=server.serve_forever, daemon=True); thread.start()
    try:
        ports = f"9999,{closed_port}"
        first = run_cli("syn_scan.py", "--ports", ports, "--json")
        states = {r["port"]: r["state"] for r in first["observations"]}
        assert states == {9999: "OPEN", closed_port: "CLOSED"}, first
        assert len(requests) == 0
        with socket.create_connection((TARGET, 9999), timeout=2):
            pass
        # The successful connect above sends no HTTP request.
        with urllib.request.urlopen(f"http://{TARGET}:9999/", timeout=2) as response:
            assert response.status == 200 and response.read() == b"step080\n"
        assert len(requests) == 1, requests
        echo_result = run_cli("packet_lab.py", "echo"); assert echo_result["matchingEchoReply"]
        source_result = run_cli("packet_lab.py", "source")
        assert source_result["matchingEchoReply"] and source_result["replyDestination"] == "127.0.0.2"
        capture = run_cli("packet_lab.py", "sniff")
        assert {(x[0], x[2]) for x in capture["uniqueTypeIdSequence"]} == {(t,s) for t in [0,8] for s in [1,2,3]}
        # A local RST normally elicits no reply. This is NOT a filtered-SYN test.
        unanswered = sr1(IP(src=TARGET,dst=TARGET)/TCP(sport=40000,dport=closed_port,flags="R"), timeout=0.3, verbose=0)
        assert unanswered is None
        server.shutdown(); server.server_close()
        after = run_cli("syn_scan.py", "--ports", "9999", "--json")
        assert after["observations"][0]["state"] == "CLOSED"
        invalid = []
        for value in ["0", "65536", "abc", "", ",", ",".join(map(str,range(1,34)))]:
            p = subprocess.run([sys.executable,str(ROOT/'syn_scan.py'),'--ports',value],capture_output=True,timeout=10)
            assert p.returncode == 2; invalid.append(value)
        return {"initial": first, "afterServerStop": after, "matchingEcho": echo_result,
                "changedLocalSource": source_result, "capture": capture,
                "httpAccessLog": {"afterSYN": 0, "afterConnectAndOneGET":len(requests), "entries":requests},
                "noReplyToLocalRST": True, "filteredSYNReproduced":False, "invalidCLIInputs":len(invalid)}
    finally:
        server.shutdown(); server.server_close(); closed.close(); thread.join(timeout=2)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--offline',action='store_true')
    args = parser.parse_args()
    result = {"at":time.time(),"python":platform.python_version(),"platform":platform.platform(),
              "scapy":scapy.__version__,"offline":offline()}
    if not args.offline:
        result['live'] = live()
    result['sources'] = {n:hashlib.sha256((ROOT/n).read_bytes()).hexdigest() for n in ['syn_scan.py','packet_lab.py','reproduce.py']}
    result['status'] = 'pass'
    print(json.dumps(result,indent=2))


if __name__ == '__main__':
    main()
