"""Verify zoom/pan coordinate math:
1. Drawing under zoom maps normalized coords correctly (shell-rect based)
2. Pinch-zoom scales AND keeps the content under the finger midpoint pinned
3. Transform persists across modal open/close
"""
import os
import sys
import time

sys.stdout.reconfigure(encoding="utf-8", errors="replace")

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service

HERE = os.path.dirname(os.path.abspath(__file__))
GECKODRIVER = os.path.join(HERE, "geckodriver.exe")
BASE = "http://localhost:8000/field-capture.html?debug"

# Compute client coords from the SHELL rect (untransformed) + current transform,
# exactly like a real finger on screen. Returns nothing; draws the line.
DRAW_JS = """
return (function(startX, startY, endX, endY, pointerId){
  const canvas = document.getElementById('sketchCanvas');
  const view = document.getElementById('sketchView');
  canvas.setPointerCapture = function(){};
  canvas.releasePointerCapture = function(){};
  canvas.hasPointerCapture = function(){ return false; };
  const shell = document.querySelector('.sketch-shell');
  const sr = shell.getBoundingClientRect();
  const left = sr.left + shell.clientLeft;
  const top = sr.top + shell.clientTop;
  const T = window.__test;
  const v = T.ui.viewState[T.ui.sketchId] || { scale: 1, tx: 0, ty: 0 };
  const w = view.offsetWidth, h = view.offsetHeight;
  const toClient = (nx, ny) => {
    return { x: left + v.tx + nx * w * v.scale, y: top + v.ty + ny * h * v.scale };
  };
  const s = toClient(startX, startY), e = toClient(endX, endY);
  const opts = { bubbles: true, cancelable: true, pointerType: 'touch', isPrimary: true };
  canvas.dispatchEvent(new PointerEvent('pointerdown', Object.assign({}, opts, {pointerId: pointerId, clientX: s.x, clientY: s.y})));
  for (let i = 1; i <= 5; i++) canvas.dispatchEvent(new PointerEvent('pointermove', Object.assign({}, opts, {pointerId: pointerId, clientX: s.x+(e.x-s.x)*i/5, clientY: s.y+(e.y-s.y)*i/5})));
  canvas.dispatchEvent(new PointerEvent('pointerup', Object.assign({}, opts, {pointerId: pointerId, clientX: e.x, clientY: e.y})));
  return JSON.stringify({ sx: s.x, sy: s.y, ex: e.x, ey: e.y, scale: v.scale, tx: v.tx, ty: v.ty });
})(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
"""

# Two-finger pinch: fingers start at offsets (dx1,dy1)/(dx2,dy2) from shell center
# (fraction of shell w/h), then move to (dx1*mult, dy1*mult)/(dx2*mult, dy2*mult)
# so the midpoint stays fixed at the shell center. Returns transforms before/after
# plus the normalized sketch point under the shell center before/after.
PINCH_JS = """
return (function(dx1, dy1, dx2, dy2, mult, id1, id2){
  const canvas = document.getElementById('sketchCanvas');
  const view = document.getElementById('sketchView');
  canvas.setPointerCapture = function(){};
  canvas.releasePointerCapture = function(){};
  canvas.hasPointerCapture = function(){ return false; };
  const shell = document.querySelector('.sketch-shell');
  const sr = shell.getBoundingClientRect();
  const left = sr.left + shell.clientLeft, top = sr.top + shell.clientTop;
  const w = view.offsetWidth, h = view.offsetHeight;
  const T = window.__test;
  const v = T.ui.viewState[T.ui.sketchId] || { scale: 1, tx: 0, ty: 0 };
  const cx = left + w/2, cy = top + h/2;
  const opts = { bubbles: true, cancelable: true, pointerType: 'touch' };
  const normUnder = () => {
    const vv = T.ui.viewState[T.ui.sketchId];
    const nx = ((cx - left - vv.tx) / vv.scale) / w;
    const ny = ((cy - top - vv.ty) / vv.scale) / h;
    return { nx, ny };
  };
  const before = { scale: v.scale, tx: v.tx, ty: v.ty, under: normUnder() };
  const p1 = { x: cx + dx1*w, y: cy + dy1*h };
  const p2 = { x: cx + dx2*w, y: cy + dy2*h };
  canvas.dispatchEvent(new PointerEvent('pointerdown', Object.assign({}, opts, {pointerId: id1, clientX: p1.x, clientY: p1.y})));
  canvas.dispatchEvent(new PointerEvent('pointerdown', Object.assign({}, opts, {pointerId: id2, clientX: p2.x, clientY: p2.y})));
  const e1 = { x: cx + dx1*w*mult, y: cy + dy1*h*mult };
  const e2 = { x: cx + dx2*w*mult, y: cy + dy2*h*mult };
  for (let i = 1; i <= 6; i++) {
    canvas.dispatchEvent(new PointerEvent('pointermove', Object.assign({}, opts, {pointerId: id1, clientX: p1.x+(e1.x-p1.x)*i/6, clientY: p1.y+(e1.y-p1.y)*i/6})));
    canvas.dispatchEvent(new PointerEvent('pointermove', Object.assign({}, opts, {pointerId: id2, clientX: p2.x+(e2.x-p2.x)*i/6, clientY: p2.y+(e2.y-p2.y)*i/6})));
  }
  canvas.dispatchEvent(new PointerEvent('pointerup', Object.assign({}, opts, {pointerId: id1, clientX: e1.x, clientY: e1.y})));
  canvas.dispatchEvent(new PointerEvent('pointerup', Object.assign({}, opts, {pointerId: id2, clientX: e2.x, clientY: e2.y})));
  const after = T.ui.viewState[T.ui.sketchId];
  return JSON.stringify({ before, after: { scale: after.scale, tx: after.tx, ty: after.ty, under: normUnder() } });
})(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5], arguments[6]);
"""


def main():
    opts = Options()
    opts.headless = False
    svc = Service(executable_path=GECKODRIVER)
    d = webdriver.Firefox(options=opts, service=svc)
    d.set_window_size(480, 900)
    try:
        d.get(BASE)
        time.sleep(2)
        d.find_element(By.CSS_SELECTOR, '[data-action="newjob"]').click()
        time.sleep(0.8)
        d.find_element(By.CSS_SELECTOR, '[data-action="opensketch"]').click()
        time.sleep(0.8)
        d.execute_script("""
            const T = window.__test;
            const job = T.state.jobs[0];
            const sk = job.sketches[0];
            const c = document.createElement('canvas');
            c.width = 200; c.height = 150;
            const ctx = c.getContext('2d');
            ctx.fillStyle = '#cccccc'; ctx.fillRect(0,0,200,150);
            sk.photo = { dataUrl: c.toDataURL('image/jpeg', 0.8), width: 200, height: 150 };
            T.saveState();
        """)
        d.execute_script(
            "const T=window.__test; T.ui.screen='sketch'; T.ui.sketchId=T.state.jobs[0].sketches[0].id; T.render();"
        )
        time.sleep(0.8)

        # 1. Draw a line at normalized (0.3,0.4)->(0.7,0.6) at 1x zoom
        print("draw@1x:", d.execute_script(DRAW_JS, 0.3, 0.4, 0.7, 0.6, 21))
        time.sleep(0.6)
        d.execute_script('document.querySelector(".sheet-minimal [data-action=closesheet]").click()')
        time.sleep(0.4)
        line = d.execute_script("""
            const T=window.__test; const l=T.state.jobs[0].sketches[0].lines[0];
            return JSON.stringify({start: l.start, end: l.end});
        """)
        print("line@1x:", line)
        import json
        l1 = json.loads(line)
        ok1 = abs(l1["start"]["x"] - 0.3) < 0.01 and abs(l1["start"]["y"] - 0.4) < 0.01 \
              and abs(l1["end"]["x"] - 0.7) < 0.01 and abs(l1["end"]["y"] - 0.6) < 0.01
        print("PASS draw@1x" if ok1 else "FAIL draw@1x")

        # 2. Pinch-zoom in the center: fingers at ±0.2 of shell, distance x2
        pinch = d.execute_script(PINCH_JS, -0.2, 0.0, 0.2, 0.0, 2.0, 31, 32)
        print("pinch:", pinch)
        time.sleep(0.6)
        p = json.loads(pinch)
        # The anchor is correct when the sketch point under the pinch midpoint
        # is IDENTICAL before and after the zoom (content stays pinned under
        # the fingers). The view is photo-sized so it may not be 0.5 — the
        # stable-under check is what proves anchoring.
        ok2 = p["after"]["scale"] > 1.9 and p["after"]["scale"] < 2.1 \
              and abs(p["after"]["under"]["nx"] - p["before"]["under"]["nx"]) < 0.02 \
              and abs(p["after"]["under"]["ny"] - p["before"]["under"]["ny"]) < 0.02
        print("PASS pinch anchor" if ok2 else "FAIL pinch anchor")

        # 3. Draw a line under zoom at normalized (0.2,0.2)->(0.5,0.5);
        #    it must land at exactly those normalized coords.
        print("draw@zoom:", d.execute_script(DRAW_JS, 0.2, 0.2, 0.5, 0.5, 22))
        time.sleep(0.6)
        line2 = d.execute_script("""
            const T=window.__test; const l=T.state.jobs[0].sketches[0].lines[1];
            return JSON.stringify({start: l.start, end: l.end});
        """)
        print("line@zoom:", line2)
        l2 = json.loads(line2)
        ok3 = abs(l2["start"]["x"] - 0.2) < 0.01 and abs(l2["start"]["y"] - 0.2) < 0.01 \
              and abs(l2["end"]["x"] - 0.5) < 0.01 and abs(l2["end"]["y"] - 0.5) < 0.01
        print("PASS draw@zoom" if ok3 else "FAIL draw@zoom")
        d.execute_script('document.querySelector(".sheet-minimal [data-action=closesheet]").click()')
        time.sleep(0.4)

        # 4. Transform preserved after modal close
        t4 = d.execute_script("""
            const T = window.__test;
            const sk = T.state.jobs[0].sketches[0];
            const v = T.ui.viewState[sk.id];
            const view = document.getElementById('sketchView');
            return JSON.stringify({scale: v.scale, tx: v.tx, ty: v.ty, transform: view.style.transform});
        """)
        print("transform after close:", t4)
        j4 = json.loads(t4)
        ok4 = j4["scale"] > 1.9 and j4["scale"] < 2.1
        print("PASS transform persists" if ok4 else "FAIL transform persists")
    finally:
        d.quit()


if __name__ == "__main__":
    main()