"""Regression test for the undo bug: after create -> copy-drag -> endpoint move,
undo must revert the endpoint move (the most recent action), NOT the copy-drag.

Reproduces the user's report: undo skipped the endpoint move and instead removed
the copied line. With the unified snapshot history, undo reverts the last action
regardless of type.
"""
import os
import sys
import time

sys.stdout.reconfigure(encoding="utf-8", errors="replace")

from selenium import webdriver
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"

DRAW_JS = """
return (function(startX, startY, endX, endY, pointerId){
  const canvas = document.getElementById('sketchCanvas');
  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 view = document.getElementById('sketchView');
  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) => ({ 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 'drawn';
})(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
"""

def copy_drag(d, line_idx, dx, dy, pointer_id, hold_s=0.6):
    d.execute_script("""
        return (function(lineIdx, pointerId){
          const canvas = document.getElementById('sketchCanvas');
          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 view = document.getElementById('sketchView');
          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 sk = T.state.jobs[0].sketches[0];
          const line = sk.lines[lineIdx];
          const mx = left + v.tx + ((line.start.x + line.end.x) / 2) * w * v.scale;
          const my = top + v.ty + ((line.start.y + line.end.y) / 2) * h * v.scale;
          const opts = { bubbles: true, cancelable: true, pointerType: 'touch', isPrimary: true };
          canvas.dispatchEvent(new PointerEvent('pointerdown', Object.assign({}, opts, {pointerId: pointerId, clientX: mx, clientY: my})));
          return 'down';
        })(arguments[0], arguments[1]);
    """, line_idx, pointer_id)
    time.sleep(hold_s)
    d.execute_script("""
        return (function(lineIdx, dx, dy, pointerId){
          const canvas = document.getElementById('sketchCanvas');
          const shell = document.querySelector('.sketch-shell');
          const sr = shell.getBoundingClientRect();
          const left = sr.left + shell.clientLeft, top = sr.top + shell.clientTop;
          const view = document.getElementById('sketchView');
          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 sk = T.state.jobs[0].sketches[0];
          const line = sk.lines[lineIdx];
          const mx = left + v.tx + ((line.start.x + line.end.x) / 2) * w * v.scale;
          const my = top + v.ty + ((line.start.y + line.end.y) / 2) * h * v.scale;
          const ex = left + v.tx + ((line.start.x + line.end.x) / 2 + dx) * w * v.scale;
          const ey = top + v.ty + ((line.start.y + line.end.y) / 2 + dy) * h * v.scale;
          const opts = { bubbles: true, cancelable: true, pointerType: 'touch', isPrimary: true };
          for (let i = 1; i <= 5; i++) canvas.dispatchEvent(new PointerEvent('pointermove', Object.assign({}, opts, {pointerId: pointerId, clientX: mx+(ex-mx)*i/5, clientY: my+(ey-my)*i/5})));
          canvas.dispatchEvent(new PointerEvent('pointerup', Object.assign({}, opts, {pointerId: pointerId, clientX: ex, clientY: ey})));
          return 'copied';
        })(arguments[0], arguments[1], arguments[2], arguments[3]);
    """, line_idx, dx, dy, pointer_id)

def endpoint_drag(d, lx, ly, ex, ey, pointer_id, hold_s=0.6):
    """Long-press near an endpoint at (lx,ly) then drag to (ex,ey)."""
    d.execute_script("""
        return (function(lx, ly, pointerId){
          const canvas = document.getElementById('sketchCanvas');
          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 view = document.getElementById('sketchView');
          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 x = left + v.tx + lx * w * v.scale, y = top + v.ty + ly * h * v.scale;
          const opts = { bubbles: true, cancelable: true, pointerType: 'touch', isPrimary: true };
          canvas.dispatchEvent(new PointerEvent('pointerdown', Object.assign({}, opts, {pointerId: pointerId, clientX: x, clientY: y})));
          return 'down';
        })(arguments[0], arguments[1], arguments[2]);
    """, lx, ly, pointer_id)
    time.sleep(hold_s)
    d.execute_script("""
        return (function(lx, ly, ex, ey, pointerId){
          const canvas = document.getElementById('sketchCanvas');
          const shell = document.querySelector('.sketch-shell');
          const sr = shell.getBoundingClientRect();
          const left = sr.left + shell.clientLeft, top = sr.top + shell.clientTop;
          const view = document.getElementById('sketchView');
          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 x = left + v.tx + lx * w * v.scale, y = top + v.ty + ly * h * v.scale;
          const xx = left + v.tx + ex * w * v.scale, yy = top + v.ty + ey * h * v.scale;
          const opts = { bubbles: true, cancelable: true, pointerType: 'touch', isPrimary: true };
          for (let i = 1; i <= 5; i++) canvas.dispatchEvent(new PointerEvent('pointermove', Object.assign({}, opts, {pointerId: pointerId, clientX: x+(xx-x)*i/5, clientY: y+(yy-y)*i/5})));
          canvas.dispatchEvent(new PointerEvent('pointerup', Object.assign({}, opts, {pointerId: pointerId, clientX: xx, clientY: yy})));
          return 'moved';
        })(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
    """, lx, ly, ex, ey, pointer_id)

def get_lines():
    return d.execute_script("""
        const T=window.__test; const sk=T.state.jobs[0].sketches[0];
        return JSON.stringify(sk.lines.map(l => ({id: l.id, start: l.start, end: l.end})));
    """)

def undo():
    d.execute_script('document.querySelector("[data-action=undo-line]").click()')
    time.sleep(0.4)

def redo():
    d.execute_script('document.querySelector("[data-action=redo-line]").click()')
    time.sleep(0.4)

def main():
    opts = Options()
    opts.headless = False
    svc = Service(executable_path=GECKODRIVER)
    global d
    d = webdriver.Firefox(options=opts, service=svc)
    try:
        d.get(BASE)
        time.sleep(2)
        d.execute_script('document.querySelector("[data-action=newjob]").click()')
        time.sleep(0.6)
        d.execute_script('document.querySelector("[data-action=opensketch]").click()')
        time.sleep(0.6)
        d.execute_script("""
            const T = window.__test;
            const sk = T.state.jobs[0].sketches[0];
            const c = document.createElement('canvas'); c.width=200; c.height=150;
            const ctx = c.getContext('2d'); ctx.fillStyle='#ccc'; 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.6)

        # 1. Draw line 1: (0.2,0.3) -> (0.5,0.4)
        d.execute_script(DRAW_JS, 0.2, 0.3, 0.5, 0.4, 5)
        time.sleep(0.5)
        d.execute_script('document.querySelector(".sheet-minimal [data-action=closesheet]").click()')
        time.sleep(0.4)
        print("after draw L1:", get_lines())

        # 2. Copy-drag from L1's tag -> creates L2
        copy_drag(d, 0, 0.2, 0.1, 6)
        time.sleep(0.5)
        d.execute_script('document.querySelector(".sheet-minimal [data-action=closesheet]").click()')
        time.sleep(0.4)
        print("after copy-drag (expect 2 lines):", get_lines())

        # 3. Endpoint-drag L1's START endpoint (0.2,0.3) -> (0.1,0.5)
        endpoint_drag(d, 0.2, 0.3, 0.1, 0.5, 7)
        time.sleep(0.4)
        print("after endpoint move (expect L1 start->0.1,0.5, 2 lines):", get_lines())

        # 4. UNDO -> must revert the endpoint move, keeping both lines
        undo()
        lines = get_lines()
        print("after undo (expect L1 back to 0.2,0.3; 2 lines):", lines)
        n = d.execute_script("const T=window.__test; return T.state.jobs[0].sketches[0].lines.length;")
        l1 = d.execute_script("const T=window.__test; return T.state.jobs[0].sketches[0].lines[0];")
        print("line count after undo:", n)
        print("L1 start after undo:", l1["start"])
        assert n == 2, "BUG: undo removed a line instead of reverting the endpoint move"
        assert abs(l1["start"]["x"] - 0.2) < 0.001 and abs(l1["start"]["y"] - 0.3) < 0.001, "BUG: endpoint move not reverted by undo"

        # 5. REDO -> endpoint move reapplied
        redo()
        l1 = d.execute_script("const T=window.__test; return T.state.jobs[0].sketches[0].lines[0];")
        print("L1 start after redo:", l1["start"])
        assert abs(l1["start"]["x"] - 0.1) < 0.001 and abs(l1["start"]["y"] - 0.5) < 0.001, "BUG: redo did not reapply endpoint move"
        print("PASS: undo/redo correctly target the endpoint move")
    finally:
        d.quit()


if __name__ == "__main__":
    main()
