#!/usr/bin/env python3
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile


ROOT = Path(__file__).resolve().parent
FILTER = ROOT / "pantum-m7100dw-ps-filter"
SAMPLE = ROOT / "samples" / "smoke.ps"
OUTDIR = ROOT / "build" / "check"
MODEL_MAP = ROOT / "supported_models.tsv"
SANE_BACKEND = ROOT / "sane" / "lib" / "x86_64-linux-gnu" / "libsane-pantum6500.so.1.0.24"
SANE_CONF = ROOT / "sane" / "pantum6500.conf"
SANE_DLL = ROOT / "sane" / "dll.d" / "pantum6500"
UDEV_RULES = ROOT / "udev" / "60-pantum-m7100.rules"
DRIVERLESS_CHECK = ROOT / "driverless-scan" / "pantum-m7100dw-driverless-scan-check"
DRIVERLESS_README = ROOT / "driverless-scan" / "README.md"
ODD_DUPLEX_TEST_PPD = ROOT / "ppd" / "Pantum M7100DW Series PS.ppd"
USB_IDS = [
    "0ecf",
    "0ed0",
    "0f62",
    "0f00",
    "0f03",
    "ff53",
    "0f59",
    "0f6c",
    "0fac",
    "0fb6",
]

SIMPLEX_SETUP = (
    b"%%BeginFeature: *Duplex None\n"
    b"<</Duplex false /Tumble false>>setpagedevice\n"
    b"%%EndFeature\n"
)
PJL_WRAPPER = (
    b"\x1b%-12345X@PJL JOB\n"
    b"@PJL SET USERNAME=\"hao\"\n"
    b"@PJL SET RESOLUTION=600\n"
    b"@PJL ENTER LANGUAGE=POSTSCRIPT\n"
)

CASES = [
    ("file-a4", "PageSize=A4", True, "simplex", None),
    ("file-duplex", "PageSize=A4 Duplex=DuplexNoTumble", True, "unchanged", None),
    ("file-duplex-tumble", "PageSize=A4 Duplex=DuplexTumble", True, "unchanged", None),
    ("file-sides-long", "PageSize=A4 sides=two-sided-long-edge", True, "unchanged", None),
    ("file-sides-short", "PageSize=A4 sides=two-sided-short-edge", True, "unchanged", None),
    ("file-sides-one", "PageSize=A4 sides=one-sided", True, "simplex", None),
    ("file-false-duplex-token", "PageSize=A4 NotDuplex=DuplexNoTumble", True, "simplex", None),
    (
        "file-manual-thick",
        "PageSize=A4 ptInputSlot=ManualFeed MediaType=Thick",
        True,
        "simplex",
        None,
    ),
    ("stdin-a4", "PageSize=A4", False, "simplex", None),
    ("stdin-pjl-a4", "PageSize=A4", False, "simplex", PJL_WRAPPER),
]


def supported_models():
    models = []
    for raw in MODEL_MAP.read_text(encoding="utf-8").splitlines():
        if not raw or raw.startswith("#"):
            continue
        fields = raw.split("\t")
        if len(fields) != 4:
            raise ValueError(f"bad model row: {raw!r}")
        models.append(fields[0])
    return models


def validate_ppds() -> bool:
    ok = True
    models = supported_models()

    for model in models:
        path = ROOT / "ppd" / f"Pantum {model} Series PS.ppd"
        if not path.exists():
            print(f"FAIL ppd {model}: missing {path}")
            ok = False
            continue

        text = path.read_text(encoding="utf-8", errors="surrogateescape")
        checks = {
            "ptps filter": '*cupsFilter: "application/vnd.cups-postscript 0 ptps"' in text,
            "PS3 device id": "CMD:PS3" in text,
            "model name": f'*ModelName: "Pantum {model} Series PS"' in text,
            "nickname": f'*NickName: "Pantum {model} Series PS"' in text,
            "device mdl": f"MDL:{model} series;" in text,
            "jcl ui": "*JCLOpenUI *Resolution" in text,
            "density ui": "*JCLOpenUI *Density" in text,
            "media ui": "*JCLOpenUI *MediaType" in text,
            "input slot ui": "*JCLOpenUI *ptInputSlot" in text,
            "feed edge ui": "*JCLOpenUI *FeedEdge" in text,
            "duplex ui": "*OpenUI *Duplex" in text,
            "default simplex": "*DefaultDuplex: None" in text,
            "manual copies": "*cupsManualCopies: True" in text,
            "even duplex": "*cupsEvenDuplex: true" in text,
        }

        failed = [name for name, passed in checks.items() if not passed]
        if failed:
            print(f"FAIL ppd {model}: {', '.join(failed)}")
            ok = False
        else:
            print(f"PASS ppd {model}: ptps PS3 metadata")

    print(f"checked {len(models)} supported PPDs")
    return ok


def validate_ppd_syntax() -> bool:
    tool = shutil.which("cupstestppd")
    if not tool:
        print("SKIP cupstestppd: command not found")
        return True

    ppds = sorted(str(path) for path in (ROOT / "ppd").glob("*.ppd"))
    proc = subprocess.run(
        [tool, "-W", "all", *ppds],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        check=False,
    )

    if proc.returncode != 0:
        print(proc.stdout)
        print(f"FAIL cupstestppd: returned {proc.returncode}")
        return False

    print(f"PASS cupstestppd: {len(ppds)} PPDs parse successfully")
    return True


def validate_sane_usb_integration() -> bool:
    ok = True
    required_files = [SANE_BACKEND, SANE_CONF, SANE_DLL, UDEV_RULES]

    for path in required_files:
        if not path.exists():
            print(f"FAIL sane/udev: missing {path}")
            ok = False

    if not ok:
        return False

    dll_text = SANE_DLL.read_text(encoding="utf-8").strip()
    if dll_text != "pantum6500":
        print(f"FAIL sane dll: expected pantum6500, got {dll_text!r}")
        ok = False

    sane_text = SANE_CONF.read_text(encoding="utf-8", errors="surrogateescape").lower()
    udev_text = UDEV_RULES.read_text(encoding="utf-8", errors="surrogateescape").lower()
    for usb_id in USB_IDS:
        if f"0x{usb_id}" not in sane_text:
            print(f"FAIL sane conf: missing USB PID {usb_id}")
            ok = False
        if f'idproduct}}=="{usb_id}"' not in udev_text:
            print(f"FAIL udev rules: missing USB PID {usb_id}")
            ok = False

    if ok:
        print(f"PASS sane/udev: pantum6500 backend and {len(USB_IDS)} USB IDs")
    return ok


def validate_driverless_scan_helper() -> bool:
    ok = True
    for path in [DRIVERLESS_CHECK, DRIVERLESS_README]:
        if not path.exists():
            print(f"FAIL driverless scan: missing {path}")
            ok = False

    if ok:
        script = DRIVERLESS_CHECK.read_text(encoding="utf-8", errors="surrogateescape")
        readme = DRIVERLESS_README.read_text(encoding="utf-8", errors="surrogateescape")
        checks = {
            "scanimage check": "scanimage -L" in script,
            "ipp-usb check": "ipp-usb" in script,
            "eSCL endpoint check": "/eSCL/ScannerCapabilities" in script,
            "driverless readme": all(term in readme for term in ["eSCL", "AirScan", "ipp-usb"]),
        }
        failed = [name for name, passed in checks.items() if not passed]
        if failed:
            print(f"FAIL driverless scan: {', '.join(failed)}")
            ok = False

    if ok:
        print("PASS driverless scan: eSCL/AirScan/ipp-usb helper files")
    return ok


def validate_odd_duplex_copy_padding() -> bool:
    tool = shutil.which("cupsfilter")
    if not tool:
        print("SKIP odd duplex copies: cupsfilter command not found")
        return True
    if not ODD_DUPLEX_TEST_PPD.exists():
        print(f"FAIL odd duplex copies: missing {ODD_DUPLEX_TEST_PPD}")
        return False

    odd_job = b"""%!PS-Adobe-3.0
%%Creator: odd duplex copy regression
%%Pages: 3
%%BoundingBox: 0 0 595 842
%%EndComments
%%Page: 1 1
/Helvetica findfont 24 scalefont setfont
72 720 moveto (copy-boundary page 1) show
showpage
%%Page: 2 2
/Helvetica findfont 24 scalefont setfont
72 720 moveto (copy-boundary page 2) show
showpage
%%Page: 3 3
/Helvetica findfont 24 scalefont setfont
72 720 moveto (copy-boundary page 3) show
showpage
%%Trailer
%%Pages: 3
%%EOF
"""

    with tempfile.NamedTemporaryFile(suffix=".ps") as tmp:
        tmp.write(odd_job)
        tmp.flush()
        proc = subprocess.run(
            [
                tool,
                "-P",
                str(ODD_DUPLEX_TEST_PPD),
                "-i",
                "application/postscript",
                "-m",
                "application/vnd.cups-postscript",
                "-n",
                "2",
                "-o",
                "Duplex=DuplexNoTumble",
                tmp.name,
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=False,
        )

    (OUTDIR / "odd-duplex-copies.ps").write_bytes(proc.stdout)
    (OUTDIR / "odd-duplex-copies.stderr.log").write_bytes(proc.stderr)

    if proc.returncode != 0:
        sys.stderr.write(proc.stderr.decode("utf-8", errors="replace"))
        print(f"FAIL odd duplex copies: cupsfilter returned {proc.returncode}")
        return False

    filler_count = proc.stdout.count(b"%%Page: (filler) 4")
    first_page_count = proc.stdout.count(b"copy-boundary page 1")
    if filler_count != 2 or first_page_count != 2:
        print(
            "FAIL odd duplex copies: expected two padded copies "
            f"(filler={filler_count}, first-pages={first_page_count})"
        )
        return False

    print("PASS odd duplex copies: CUPS pads odd-page duplex copies with filler pages")
    return True


def validate_odd_simplex_copy_no_padding() -> bool:
    tool = shutil.which("cupsfilter")
    if not tool:
        print("SKIP odd simplex copies: cupsfilter command not found")
        return True
    if not ODD_DUPLEX_TEST_PPD.exists():
        print(f"FAIL odd simplex copies: missing {ODD_DUPLEX_TEST_PPD}")
        return False

    odd_job = b"""%!PS-Adobe-3.0
%%Creator: odd simplex copy regression
%%Pages: 3
%%BoundingBox: 0 0 595 842
%%EndComments
%%Page: 1 1
/Helvetica findfont 24 scalefont setfont
72 720 moveto (simplex-copy page 1) show
showpage
%%Page: 2 2
/Helvetica findfont 24 scalefont setfont
72 720 moveto (simplex-copy page 2) show
showpage
%%Page: 3 3
/Helvetica findfont 24 scalefont setfont
72 720 moveto (simplex-copy page 3) show
showpage
%%Trailer
%%Pages: 3
%%EOF
"""

    with tempfile.NamedTemporaryFile(suffix=".ps") as tmp:
        tmp.write(odd_job)
        tmp.flush()
        proc = subprocess.run(
            [
                tool,
                "-P",
                str(ODD_DUPLEX_TEST_PPD),
                "-i",
                "application/postscript",
                "-m",
                "application/vnd.cups-postscript",
                "-n",
                "2",
                "-o",
                "Duplex=None",
                tmp.name,
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            check=False,
        )

    (OUTDIR / "odd-simplex-copies.ps").write_bytes(proc.stdout)
    (OUTDIR / "odd-simplex-copies.stderr.log").write_bytes(proc.stderr)

    if proc.returncode != 0:
        sys.stderr.write(proc.stderr.decode("utf-8", errors="replace"))
        print(f"FAIL odd simplex copies: cupsfilter returned {proc.returncode}")
        return False

    filler_count = proc.stdout.count(b"%%Page: (filler)")
    first_page_count = proc.stdout.count(b"simplex-copy page 1")
    if filler_count != 0 or first_page_count != 2:
        print(
            "FAIL odd simplex copies: expected two copies without filler pages "
            f"(filler={filler_count}, first-pages={first_page_count})"
        )
        return False

    print("PASS odd simplex copies: CUPS does not add filler pages for one-sided copies")
    return True


def with_simplex_setup(ps: bytes) -> bytes:
    line_end = ps.find(b"\n")
    if line_end < 0:
        return SIMPLEX_SETUP + ps
    return ps[: line_end + 1] + SIMPLEX_SETUP + ps[line_end + 1 :]


def run_case(
    name: str,
    options: str,
    use_file: bool,
    mode: str,
    input_prefix,
    sample: bytes,
) -> bool:
    args = [str(FILTER), "1", "hao", name, "1", options]
    input_data = (input_prefix or b"") + sample
    expected = sample if mode == "unchanged" else with_simplex_setup(sample)
    kwargs = {
        "stdout": subprocess.PIPE,
        "stderr": subprocess.PIPE,
        "check": False,
    }

    if use_file:
        args.append(str(SAMPLE))
    else:
        kwargs["input"] = input_data

    proc = subprocess.run(args, **kwargs)

    (OUTDIR / f"{name}.ps").write_bytes(proc.stdout)
    (OUTDIR / f"{name}.stderr.log").write_bytes(proc.stderr)

    if proc.returncode != 0:
        sys.stderr.write(proc.stderr.decode("utf-8", errors="replace"))
        print(f"FAIL {name}: filter returned {proc.returncode}")
        return False

    if proc.stdout != expected:
        print(f"FAIL {name}: unexpected normalized output")
        print(f"  input={len(expected)} output={len(proc.stdout)}")
        return False

    if b"@PJL" in proc.stdout:
        print(f"FAIL {name}: PJL wrapper leaked into PostScript output")
        return False

    print(f"PASS {name}: {len(proc.stdout)} bytes {mode}")
    return True


def main() -> int:
    if not FILTER.exists():
        print(f"missing filter: {FILTER}")
        return 1
    if not SAMPLE.exists():
        print(f"missing sample: {SAMPLE}")
        return 1
    if not MODEL_MAP.exists():
        print(f"missing model map: {MODEL_MAP}")
        return 1

    OUTDIR.mkdir(parents=True, exist_ok=True)
    sample = SAMPLE.read_bytes()

    ok = validate_ppds()
    ok = validate_ppd_syntax() and ok
    ok = validate_sane_usb_integration() and ok
    ok = validate_driverless_scan_helper() and ok
    ok = validate_odd_duplex_copy_padding() and ok
    ok = validate_odd_simplex_copy_no_padding() and ok
    for name, options, use_file, mode, input_prefix in CASES:
        ok = run_case(name, options, use_file, mode, input_prefix, sample) and ok

    return 0 if ok else 1


if __name__ == "__main__":
    raise SystemExit(main())
