import { describe, it, expect } from 'vitest'
import { parseHookEvent } from '../src/http/hook.js'
import type { HookEventFull } from '../src/http/hook.js'

const SID = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

// ── Existing status / detail behaviour (must not regress) ───────────────────
// We use expect.objectContaining() so the new fields (at/eventClass/gate) do
// not break these assertion — they verify only the status-mapping contract.

describe('parseHookEvent — status mapping (regression guard)', () => {
  it('maps PreToolUse / PostToolUse / UserPromptSubmit to "working"', () => {
    for (const e of ['PreToolUse', 'PostToolUse', 'UserPromptSubmit']) {
      expect(parseHookEvent(SID, { hook_event_name: e })).toEqual(
        expect.objectContaining({ sessionId: SID, status: 'working' }),
      )
    }
  })

  it('maps PermissionRequest to "waiting" with tool_name as detail', () => {
    expect(
      parseHookEvent(SID, { hook_event_name: 'PermissionRequest', tool_name: 'Bash' }),
    ).toEqual(expect.objectContaining({ sessionId: SID, status: 'waiting', detail: 'Bash' }))
  })

  it('maps Notification(permission_prompt) → "waiting"; other Notification → "idle"', () => {
    expect(
      parseHookEvent(SID, {
        hook_event_name: 'Notification',
        notification_type: 'permission_prompt',
      }),
    ).toEqual(expect.objectContaining({ sessionId: SID, status: 'waiting' }))

    expect(
      parseHookEvent(SID, {
        hook_event_name: 'Notification',
        notification_type: 'idle_prompt',
      }),
    ).toEqual(expect.objectContaining({ sessionId: SID, status: 'idle' }))
  })

  it('maps Stop / SessionEnd to "idle"', () => {
    expect(parseHookEvent(SID, { hook_event_name: 'Stop' })).toEqual(
      expect.objectContaining({ sessionId: SID, status: 'idle' }),
    )
    expect(parseHookEvent(SID, { hook_event_name: 'SessionEnd' })).toEqual(
      expect.objectContaining({ sessionId: SID, status: 'idle' }),
    )
  })

  it('returns null for missing/empty sessionId, bad body, or unknown event', () => {
    expect(parseHookEvent(undefined, { hook_event_name: 'Stop' })).toBeNull()
    expect(parseHookEvent('', { hook_event_name: 'Stop' })).toBeNull()
    expect(parseHookEvent(SID, null)).toBeNull()
    expect(parseHookEvent(SID, 'nope')).toBeNull()
    expect(parseHookEvent(SID, { hook_event_name: 'SomethingElse' })).toBeNull()
    expect(parseHookEvent(SID, {})).toBeNull()
  })
})

// ── HookEventFull — new A4 / B4 fields ──────────────────────────────────────

describe('HookEventFull — at field (A4)', () => {
  it('includes a numeric timestamp in [before, after] around the call', () => {
    const before = Date.now()
    const ev = parseHookEvent(SID, { hook_event_name: 'Stop' }) as HookEventFull
    const after = Date.now()

    expect(ev).not.toBeNull()
    expect(typeof ev.at).toBe('number')
    expect(ev.at).toBeGreaterThanOrEqual(before)
    expect(ev.at).toBeLessThanOrEqual(after)
  })

  it('is absent from null results', () => {
    const ev = parseHookEvent(SID, { hook_event_name: 'Unknown' })
    expect(ev).toBeNull()
  })
})

describe('HookEventFull — eventClass field (A4)', () => {
  const EVENTS = [
    'PreToolUse',
    'PostToolUse',
    'UserPromptSubmit',
    'PermissionRequest',
    'Stop',
    'SessionEnd',
    'Notification',
  ]

  it('carries the raw hook_event_name for every whitelisted event', () => {
    for (const e of EVENTS) {
      const ev = parseHookEvent(SID, { hook_event_name: e }) as HookEventFull
      expect(ev).not.toBeNull()
      expect(ev.eventClass).toBe(e)
    }
  })

  it('is not present when parseHookEvent returns null', () => {
    expect(parseHookEvent(SID, { hook_event_name: 'NotAHook' })).toBeNull()
  })
})

describe('HookEventFull — toolInput passthrough (A4)', () => {
  it('includes toolInput when tool_input is present in the body', () => {
    const input = { path: '/src/foo.ts', content: 'hello world' }
    const ev = parseHookEvent(SID, {
      hook_event_name: 'PreToolUse',
      tool_input: input,
    }) as HookEventFull

    expect(ev).not.toBeNull()
    expect(ev.toolInput).toBe(input) // same reference, no copy/parse
  })

  it('passes toolInput through as-is (does not validate inner shape)', () => {
    const ev = parseHookEvent(SID, {
      hook_event_name: 'PostToolUse',
      tool_input: 'just a string',
    }) as HookEventFull

    expect(ev.toolInput).toBe('just a string')
  })

  it('omits toolInput when tool_input is absent', () => {
    const ev = parseHookEvent(SID, { hook_event_name: 'PreToolUse' }) as HookEventFull
    expect(ev).not.toBeNull()
    expect('toolInput' in ev).toBe(false)
  })

  it('includes toolInput even when its value is null', () => {
    const ev = parseHookEvent(SID, {
      hook_event_name: 'PostToolUse',
      tool_input: null,
    }) as HookEventFull
    expect(ev).not.toBeNull()
    expect('toolInput' in ev).toBe(true)
    expect(ev.toolInput).toBeNull()
  })
})

describe('HookEventFull — gate field (B4 plan-gate recognition)', () => {
  it('sets gate="plan" for PermissionRequest with ExitPlanMode tool', () => {
    const ev = parseHookEvent(SID, {
      hook_event_name: 'PermissionRequest',
      tool_name: 'ExitPlanMode',
    }) as HookEventFull

    expect(ev).not.toBeNull()
    expect(ev.gate).toBe('plan')
  })

  it('sets gate="tool" for PermissionRequest with any other tool name', () => {
    const ev = parseHookEvent(SID, {
      hook_event_name: 'PermissionRequest',
      tool_name: 'Bash',
    }) as HookEventFull

    expect(ev).not.toBeNull()
    expect(ev.gate).toBe('tool')
  })

  it('sets gate="tool" for PermissionRequest with no tool_name', () => {
    const ev = parseHookEvent(SID, { hook_event_name: 'PermissionRequest' }) as HookEventFull

    expect(ev).not.toBeNull()
    expect(ev.gate).toBe('tool')
  })

  it('omits gate for non-PermissionRequest events', () => {
    const nonPermEvents = [
      'PreToolUse',
      'PostToolUse',
      'UserPromptSubmit',
      'Stop',
      'SessionEnd',
      'Notification',
    ]
    for (const e of nonPermEvents) {
      const ev = parseHookEvent(SID, { hook_event_name: e }) as HookEventFull
      expect(ev).not.toBeNull()
      expect('gate' in ev).toBe(false)
    }
  })
})

describe('HookEventFull — detail not set for Notification (no tool_name)', () => {
  it('Notification(permission_prompt) has status=waiting but no detail', () => {
    const ev = parseHookEvent(SID, {
      hook_event_name: 'Notification',
      notification_type: 'permission_prompt',
    }) as HookEventFull

    expect(ev).not.toBeNull()
    expect(ev.status).toBe('waiting')
    expect('detail' in ev).toBe(false)
  })
})

describe('SEC-M7 — never throws on any body shape', () => {
  const malformed = [
    [],
    42,
    true,
    undefined,
    { hook_event_name: null },
    { hook_event_name: 123 },
    { hook_event_name: 'PreToolUse', tool_input: { toString: null } },
  ]

  it('returns null without throwing for every malformed body', () => {
    for (const b of malformed) {
      expect(() => parseHookEvent(SID, b)).not.toThrow()
    }
  })
})
