// P8.2d (KAR-987 §24 "E2E") — Spec b: pointercancel.
//
// Closes the SECOND P8.2b jsdom-Lückenliste item: "pointercancel wird von
// keinem Test gefeuert — Cleanup-Pfad nur strukturell bewiesen." No browser
// input API (Playwright's `page.mouse`/`page.touchscreen`, or even a real
// CDP session) can trigger a genuine OS-level pointercancel (that requires
// an actual system interruption, e.g. an incoming call reclaiming a touch
// gesture) — so, same as any real E2E suite would have to, this dispatches
// a real, browser-native `PointerEvent('pointercancel', …)` via
// `document.dispatchEvent` in-page. The TRIGGER is synthetic; the event
// CLASS, DOM dispatch mechanics, and React's handling of it are all 100%
// real browser behavior — categorically different from jsdom, which
// vsm-editor.tsx's own onUp/pointercancel listener (`document.
// addEventListener('pointercancel', onUp)`, shared with pointerup) had NEVER
// been driven through by any existing test.
//
// Flag state: `wertstromUxV2Enabled: false` (default) — same "Baustein 1
// pointer-drag is unconditional" reasoning as spec a.
import { test, expect } from '@playwright/experimental-ct-react'
import VsmEditor from '@/components/wertstrom/vsm-editor'
import { buildEditorProps } from './fixtures/vsm-fixtures'
import { nodeByName } from './helpers/locators'
import { collectConsoleErrors } from './helpers/console-errors'

test('pointercancel raeumt einen laufenden Drag auf, ohne haengenden State', async ({ mount, page }) => {
  const consoleErrors = collectConsoleErrors(page)
  const component = await mount(<VsmEditor {...buildEditorProps()} />)
  const nodeA = nodeByName(component, 'Prozess A')
  const nodeB = nodeByName(component, 'Prozess B')

  // Capture the REAL pointerId Chromium assigns to the mouse pointer, so the
  // synthetic pointercancel below targets the SAME gesture vsm-editor.tsx's
  // `activePointerIdRef` is tracking (its onUp guard: `if (e.pointerId !==
  // activePointerIdRef.current) return` would otherwise silently ignore a
  // cancel for a pointerId that doesn't match).
  await page.evaluate(() => {
    document.addEventListener(
      'pointerdown',
      (e) => {
        ;(window as unknown as { __pwPointerId?: number }).__pwPointerId = e.pointerId
      },
      { capture: true, once: true },
    )
  })

  await nodeA.hover()
  const before = await nodeA.boundingBox()
  if (!before) throw new Error('Prozess A node has no bounding box')

  await page.mouse.down()
  // `.hover()` places the real cursor at the node's CENTER — every
  // subsequent `mouse.move` target below is therefore expressed as
  // `<box>.x + <box>.width / 2 + <delta>` (center + delta), never
  // `<box>.x + <delta>` (which would silently also apply a `-width/2`
  // jump relative to the actual cursor position).
  await page.mouse.move(before.x + before.width / 2 + 80, before.y + before.height / 2 + 20, { steps: 4 })
  const midDrag = await nodeA.boundingBox()
  if (!midDrag) throw new Error('Prozess A node lost its bounding box')
  // Sanity: the drag genuinely started (node actually moved) before we
  // cancel it — otherwise a "no further movement after cancel" assertion
  // below would be vacuously true.
  expect(midDrag.x).not.toBeCloseTo(before.x, 0)

  const pointerId = await page.evaluate(() => (window as unknown as { __pwPointerId?: number }).__pwPointerId)
  if (typeof pointerId !== 'number') throw new Error('did not capture a real pointerId from the pointerdown')

  await page.evaluate((pid) => {
    document.dispatchEvent(new PointerEvent('pointercancel', { pointerId: pid, bubbles: true, cancelable: true }))
  }, pointerId)

  // The OS-level mouse button (via Playwright's CDP input state) is still
  // "held" at this point — dispatching the synthetic cancel above does not
  // itself release it. Moving the real mouse now must NOT keep dragging the
  // node: a correctly-cleaned-up `activePointerIdRef`/`drag.current` makes
  // this next real pointermove a no-op for the (already-cancelled) gesture.
  await page.mouse.move(midDrag.x + midDrag.width / 2 + 200, midDrag.y + midDrag.height / 2, { steps: 4 })
  const afterCancelMove = await nodeA.boundingBox()
  if (!afterCancelMove) throw new Error('Prozess A node lost its bounding box')
  expect(afterCancelMove.x).toBeCloseTo(midDrag.x, 0)
  expect(afterCancelMove.y).toBeCloseTo(midDrag.y, 0)

  await page.mouse.up()

  // "Folge-Interaktion sauber" (Brief): a brand-new drag on a DIFFERENT node
  // right after the cancel must behave completely normally — proving no
  // stale `drag.current`/`activePointerIdRef` survived to interfere with it.
  await nodeB.hover()
  const beforeB = await nodeB.boundingBox()
  if (!beforeB) throw new Error('Prozess B node has no bounding box')
  await page.mouse.down()
  await page.mouse.move(beforeB.x + beforeB.width / 2 + 50, beforeB.y + beforeB.height / 2 + 10, { steps: 4 })
  await page.mouse.up()
  const afterB = await nodeB.boundingBox()
  if (!afterB) throw new Error('Prozess B node lost its bounding box')
  expect(afterB.x - beforeB.x).toBeCloseTo(50, 0)
  expect(afterB.y - beforeB.y).toBeCloseTo(10, 0)

  expect(consoleErrors).toEqual([])
})
