// Task #168 / step 1 — payload-controls util tests. // // Verifies: // 1. pure helpers (prettyString / rawString / coerceForPretty / // jsonStringifySafe) behave on scalars, objects, strings-that-are- // JSON, circular refs, and undefined. // 2. attachPayloadControls mounts controls +
 into a host, wires
//      the pretty↔raw toggle, and does not throw when navigator.clipboard
//      / URL.createObjectURL are absent (headless).
//   3. Toggle click swaps the 
 text between pretty (indent=2) and
//      raw (indent=0) forms, and the button label follows.
//   4. Copy click invokes navigator.clipboard.writeText with the pretty
//      string when a stub is present.

'use strict'

const test = require('node:test')
const assert = require('node:assert/strict')

const pc = require('../src/renderer/payload-controls.js')

// ---------- fake doc (matches trace-detail-pane.test shape) --------------

function makeDoc() {
  function makeEl(tag) {
    const el = {
      tagName: (tag || 'div').toUpperCase(),
      children: [],
      _classSet: new Set(),
      dataset: {},
      style: {},
      hidden: false,
      _listeners: {},
      _attrs: {},
      _text: '',
    }
    Object.defineProperty(el, 'className', {
      get() { return Array.from(this._classSet).join(' ') },
      set(v) { this._classSet = new Set(String(v || '').split(/\s+/).filter(Boolean)) },
    })
    Object.defineProperty(el, 'textContent', {
      get() {
        if (this._text) return this._text
        let s = ''
        for (const c of this.children) s += (c.textContent || '')
        return s
      },
      set(v) { this._text = String(v == null ? '' : v); this.children = [] },
    })
    el.classList = {
      add: (c) => el._classSet.add(c),
      remove: (c) => el._classSet.delete(c),
      contains: (c) => el._classSet.has(c),
    }
    el.setAttribute = (k, v) => { el._attrs[k] = String(v) }
    el.getAttribute = (k) => (k in el._attrs ? el._attrs[k] : null)
    el.appendChild = (child) => {
      el.children.push(child)
      child.parentNode = el
      return child
    }
    el.addEventListener = (evt, fn) => { (el._listeners[evt] = el._listeners[evt] || []).push(fn) }
    el.click = () => { for (const fn of (el._listeners.click || [])) fn({ stopPropagation() {} }) }
    el.ownerDocument = doc
    return el
  }
  const doc = { createElement: (t) => makeEl(t) }
  return doc
}

function findChild(el, cls) {
  for (const c of el.children) if (c._classSet && c._classSet.has(cls)) return c
  for (const c of el.children) {
    const nested = findChild(c, cls)
    if (nested) return nested
  }
  return null
}

// ---- pure helpers -------------------------------------------------------

test('prettyString indents objects by 2 spaces', () => {
  const s = pc.prettyString({ a: 1, b: [2, 3] })
  assert.equal(s, '{\n  "a": 1,\n  "b": [\n    2,\n    3\n  ]\n}')
})

test('rawString is single-line JSON.stringify', () => {
  const s = pc.rawString({ a: 1, b: [2, 3] })
  assert.equal(s, '{"a":1,"b":[2,3]}')
})

test('coerceForPretty re-parses JSON strings so pretty view shows structure', () => {
  const s = pc.prettyString('{"a":1}')
  assert.equal(s, '{\n  "a": 1\n}')
})

test('coerceForPretty leaves plain strings alone', () => {
  assert.equal(pc.prettyString('hello world'), '"hello world"')
})

test('jsonStringifySafe survives circular refs', () => {
  const a = { name: 'a' }; a.self = a
  const s = pc.prettyString(a)
  assert.match(s, /"self": "\[Circular\]"/)
})

test('undefined renders as (absent) marker (zero-drop rule)', () => {
  assert.equal(pc.prettyString(undefined), '(absent)')
  assert.equal(pc.rawString(undefined), '(absent)')
})

// ---- DOM composition ----------------------------------------------------

test('attachPayloadControls mounts controls + pre into host', () => {
  const doc = makeDoc()
  const host = doc.createElement('div')
  const payload = { command: 'echo hi', env: { X: '1' } }
  const ret = pc.attachPayloadControls(host, {
    getRaw: () => payload,
    kind: 'args',
  })
  assert.ok(ret, 'returns handle')
  const ctl = findChild(host, 'payload-controls')
  const pre = findChild(host, 'payload-body')
  assert.ok(ctl, 'controls container mounted')
  assert.ok(pre, 'pre body mounted')
  assert.equal(ctl.getAttribute('data-payload-kind'), 'args')
  assert.ok(findChild(ctl, 'payload-ctl-toggle'), 'toggle present')
  assert.ok(findChild(ctl, 'payload-ctl-copy'), 'copy present')
  assert.ok(findChild(ctl, 'payload-ctl-download'), 'download present')
  assert.match(pre.textContent, /"command": "echo hi"/, 'starts in pretty mode')
})

test('toggle swaps pretty↔raw text and label', () => {
  const doc = makeDoc()
  const host = doc.createElement('div')
  pc.attachPayloadControls(host, { getRaw: () => ({ a: 1 }), kind: 'args' })
  const toggle = findChild(host, 'payload-ctl-toggle')
  const pre = findChild(host, 'payload-body')
  assert.equal(toggle.textContent, 'pretty')
  toggle.click()
  assert.equal(toggle.textContent, 'raw')
  assert.equal(pre.textContent, '{"a":1}')
  toggle.click()
  assert.equal(toggle.textContent, 'pretty')
  assert.match(pre.textContent, /\n {2}"a": 1/)
})

test('copy click routes through navigator.clipboard.writeText when present', () => {
  const doc = makeDoc()
  const host = doc.createElement('div')
  let captured = null
  // Node 20+ exposes a read-only `navigator` global; poke `clipboard` onto
  // it via defineProperty (no clipboard is defined by default in node).
  // If a native clipboard already exists we shim writeText onto it.
  const clipDescOrig = Object.getOwnPropertyDescriptor(globalThis.navigator || {}, 'clipboard') || null
  const nav = globalThis.navigator
  const origClipboard = nav.clipboard
  try {
    Object.defineProperty(nav, 'clipboard', {
      configurable: true,
      value: { writeText: (s) => { captured = s; return Promise.resolve() } },
    })
    pc.attachPayloadControls(host, { getRaw: () => ({ ok: true }), kind: 'args' })
    const copy = findChild(host, 'payload-ctl-copy')
    copy.click()
    assert.match(captured, /"ok": true/)
  } finally {
    if (clipDescOrig) Object.defineProperty(nav, 'clipboard', clipDescOrig)
    else if (origClipboard === undefined) delete nav.clipboard
    else Object.defineProperty(nav, 'clipboard', { configurable: true, value: origClipboard })
  }
})

test('startMode=raw starts with raw text and toggle label', () => {
  const doc = makeDoc()
  const host = doc.createElement('div')
  pc.attachPayloadControls(host, { getRaw: () => ({ b: 2 }), kind: 'result', startMode: 'raw' })
  const toggle = findChild(host, 'payload-ctl-toggle')
  const pre = findChild(host, 'payload-body')
  assert.equal(toggle.textContent, 'raw')
  assert.equal(pre.textContent, '{"b":2}')
})

test('setRaw() refreshes the 
 without callers touching getRaw', () => {
  const doc = makeDoc()
  const host = doc.createElement('div')
  let payload = { streaming: true }
  const { setRaw } = pc.attachPayloadControls(host, { getRaw: () => payload, kind: 'result' })
  const pre = findChild(host, 'payload-body')
  assert.match(pre.textContent, /"streaming": true/)
  payload = { streaming: false, done: 42 }
  setRaw(payload)
  assert.match(pre.textContent, /"done": 42/)
})

test('null host returns null (headless / caller-error safe)', () => {
  assert.equal(pc.attachPayloadControls(null, { getRaw: () => ({}) }), null)
})

test('missing document safety: returns null, no throw', () => {
  const noOwner = { appendChild: () => {} }
  assert.equal(pc.attachPayloadControls(noOwner, { getRaw: () => ({}) }), null)
})