/**
 * test/sw-push.test.ts — Unit tests for public/sw-push.js pure helpers.
 *
 * Covers T-sw W1: buildPushNotification (all NotifyClass values) and
 * resolveNotificationClick (allow / deny / body-click routing).
 *
 * Security: SEC-C1 (decision body carries token), SEC-M6 (tag=sessionId dedup).
 * No DOM or SW globals needed — sw-push.js is intentionally pure.
 */

import { describe, it, expect } from 'vitest'
import type { PushPayload } from '../src/types.js'

// sw-push.js is a plain ES-module JS file; vitest imports it as ESM.
// We assert the expected return shapes inline.
const { buildPushNotification, resolveNotificationClick } = await import(
  '../public/sw-push.js'
)

// ─────────────── helpers ───────────────────────────────────────────────────

/** Minimal notification mock matching the shape sw-push.js reads. */
function fakeNotification(data: Record<string, unknown>): Notification {
  return { data } as unknown as Notification
}

// ─────────────── buildPushNotification ────────────────────────────────────

describe('buildPushNotification', () => {
  it('needs-input: requireInteraction=true, Allow+Deny actions, tag=sessionId', () => {
    const payload: PushPayload = {
      sessionId: 'sess-001',
      toolName: 'Bash',
      detail: 'rm -rf /tmp/cache',
      token: 'tok-aaa',
      cls: 'needs-input',
    }
    const { title, options } = buildPushNotification(payload)

    expect(title).toBeTruthy()
    expect(typeof title).toBe('string')
    expect(options.tag).toBe('sess-001') // SEC-M6
    expect(options.requireInteraction).toBe(true)
    const actions: Array<{ action: string; title: string }> = options.actions ?? []
    expect(actions.length).toBeGreaterThanOrEqual(2)
    expect(actions.some((a) => a.action === 'allow')).toBe(true)
    expect(actions.some((a) => a.action === 'deny')).toBe(true)
    expect(options.data).toMatchObject({
      sessionId: 'sess-001',
      token: 'tok-aaa',
      cls: 'needs-input',
    })
  })

  it('needs-input without detail falls back to toolName in body', () => {
    const payload: PushPayload = {
      sessionId: 's',
      toolName: 'Edit',
      token: 'tok-bbb',
      cls: 'needs-input',
    }
    const { options } = buildPushNotification(payload)
    expect(options.body ?? '').toContain('Edit')
  })

  it('done: no requireInteraction, no actions, tag=sessionId', () => {
    const payload: PushPayload = { sessionId: 'sess-002', cls: 'done' }
    const { title, options } = buildPushNotification(payload)

    expect(title).toBeTruthy()
    expect(options.tag).toBe('sess-002') // SEC-M6
    expect(options.requireInteraction).toBeFalsy()
    expect((options.actions ?? []).length).toBe(0)
    expect(options.data).toMatchObject({ sessionId: 'sess-002', cls: 'done' })
  })

  it('done: body includes detail when provided', () => {
    const payload: PushPayload = {
      sessionId: 'sess-003',
      detail: 'Build succeeded',
      cls: 'done',
    }
    const { options } = buildPushNotification(payload)
    expect(options.body).toContain('Build succeeded')
  })

  it('stuck: no requireInteraction, no actions, tag=sessionId', () => {
    const payload: PushPayload = { sessionId: 'sess-004', cls: 'stuck' }
    const { title, options } = buildPushNotification(payload)

    expect(title).toBeTruthy()
    expect(options.tag).toBe('sess-004') // SEC-M6
    expect(options.requireInteraction).toBeFalsy()
    expect((options.actions ?? []).length).toBe(0)
    expect(options.data).toMatchObject({ sessionId: 'sess-004', cls: 'stuck' })
  })

  it('no detail and no toolName → body is undefined or empty', () => {
    const payload: PushPayload = { sessionId: 'sess-005', cls: 'done' }
    const { options } = buildPushNotification(payload)
    // body should be undefined or an empty string — not a thrown error
    expect(() => buildPushNotification(payload)).not.toThrow()
    const body = options.body
    expect(body === undefined || body === '').toBe(true)
  })
})

// ─────────────── resolveNotificationClick ─────────────────────────────────

describe('resolveNotificationClick', () => {
  it('allow action → decision with decision=allow and capability token (SEC-C1)', () => {
    const notification = fakeNotification({
      sessionId: 'sess-10',
      token: 'tok-xyz',
      cls: 'needs-input',
    })
    const result = resolveNotificationClick(notification, 'allow')

    expect(result.kind).toBe('decision')
    const body = JSON.parse(result.body) as Record<string, unknown>
    expect(body.sessionId).toBe('sess-10')
    expect(body.decision).toBe('allow')
    expect(body.token).toBe('tok-xyz') // SEC-C1: token forwarded
  })

  it('deny action → decision with decision=deny', () => {
    const notification = fakeNotification({
      sessionId: 'sess-11',
      token: 'tok-abc',
      cls: 'needs-input',
    })
    const result = resolveNotificationClick(notification, 'deny')

    expect(result.kind).toBe('decision')
    const body = JSON.parse(result.body) as Record<string, unknown>
    expect(body.sessionId).toBe('sess-11')
    expect(body.decision).toBe('deny')
    expect(body.token).toBe('tok-abc')
  })

  it('empty action string (body click) → focus with session URL', () => {
    const notification = fakeNotification({ sessionId: 'sess-12' })
    const result = resolveNotificationClick(notification, '')

    expect(result.kind).toBe('focus')
    expect(result.url).toBe('/?session=sess-12')
  })

  it('undefined action (body click) → focus with session URL', () => {
    const notification = fakeNotification({ sessionId: 'sess-13' })
    const result = resolveNotificationClick(notification, undefined)

    expect(result.kind).toBe('focus')
    expect(result.url).toBe('/?session=sess-13')
  })

  it('null action → focus (treated as default body click)', () => {
    const notification = fakeNotification({ sessionId: 'sess-14' })
    const result = resolveNotificationClick(notification, null)

    expect(result.kind).toBe('focus')
    expect(result.url).toContain('sess-14')
  })

  it('token is undefined in body when not in notification data (done notification)', () => {
    // done/stuck notifications have no token in data; test edge: action='allow' still
    // sends a decision body but token field is absent (server should reject gracefully)
    const notification = fakeNotification({ sessionId: 'sess-15', cls: 'done' })
    const result = resolveNotificationClick(notification, 'allow')

    expect(result.kind).toBe('decision')
    const body = JSON.parse(result.body) as Record<string, unknown>
    expect(body.token).toBeUndefined()
  })

  it('sessionId is URL-encoded in focus URL (special characters safe)', () => {
    const notification = fakeNotification({ sessionId: 'sess a+b' })
    const result = resolveNotificationClick(notification, '')

    expect(result.kind).toBe('focus')
    // encodeURIComponent encodes spaces and + signs
    expect(result.url).not.toContain(' ')
  })

  it('missing data on notification → focus URL with empty session gracefully', () => {
    // notification.data is null/undefined
    const notification = { data: null } as unknown as Notification
    expect(() => resolveNotificationClick(notification, '')).not.toThrow()
  })
})
