import { describe, expect, it } from 'vitest'
import { validateSubmission } from './validate.js'
import type { FormDef } from './types.js'

const form: FormDef = {
  id: 'signup',
  fields: [
    { name: 'email', type: 'email', label: 'Email', required: true },
    { name: 'name', type: 'text', label: 'Name', min: 2, max: 5 },
    { name: 'age', type: 'number', label: 'Age', min: 18, max: 120 },
    { name: 'plan', type: 'select', label: 'Plan', options: [{ value: 'free', label: 'Free' }, { value: 'pro', label: 'Pro' }] },
    { name: 'tags', type: 'multiselect', label: 'Tags', options: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }], max: 2 },
    { name: 'subscribe', type: 'checkbox', label: 'Subscribe' },
  ],
}

describe('validateSubmission', () => {
  it('accepts a clean submission and coerces by type', () => {
    const r = validateSubmission(form, { email: 'a@b.co', name: 'Jo', age: '20', plan: 'pro', tags: ['a'], subscribe: 'on' })
    expect(r.ok).toBe(true)
    if (r.ok) {
      expect(r.value.age).toBe(20) // coerced to number
      expect(r.value.tags).toEqual(['a'])
      expect(r.value.subscribe).toBe(true)
    }
  })

  it('DROPS undeclared keys — never echoes raw input', () => {
    const r = validateSubmission(form, { email: 'a@b.co', evil: 'INJECT', __proto__: 'x' })
    expect(r.ok).toBe(true)
    if (r.ok) {
      expect('evil' in r.value).toBe(false)
      expect(Object.prototype.hasOwnProperty.call(r.value, 'evil')).toBe(false)
    }
  })

  it('errors on a missing required field', () => {
    const r = validateSubmission(form, { name: 'Jo' })
    expect(r.ok).toBe(false)
    if (!r.ok) expect(r.error.some((e) => e.field === 'email' && e.code === 'required')).toBe(true)
  })

  it('errors on a non-numeric number field (NaN guard)', () => {
    const r = validateSubmission(form, { email: 'a@b.co', age: 'not-a-number' })
    expect(r.ok).toBe(false)
    if (!r.ok) expect(r.error.some((e) => e.field === 'age' && e.code === 'type')).toBe(true)
  })

  it('enforces number min/max', () => {
    const r = validateSubmission(form, { email: 'a@b.co', age: '5' })
    expect(r.ok).toBe(false)
    if (!r.ok) expect(r.error.some((e) => e.field === 'age' && e.code === 'min')).toBe(true)
  })

  it('enforces text length min/max', () => {
    const r = validateSubmission(form, { email: 'a@b.co', name: 'TooLongName' })
    expect(r.ok).toBe(false)
    if (!r.ok) expect(r.error.some((e) => e.field === 'name' && e.code === 'max')).toBe(true)
  })

  it('rejects an out-of-set select option', () => {
    const r = validateSubmission(form, { email: 'a@b.co', plan: 'enterprise' })
    expect(r.ok).toBe(false)
    if (!r.ok) expect(r.error.some((e) => e.field === 'plan' && e.code === 'option')).toBe(true)
  })

  it('rejects a malformed email (type)', () => {
    const r = validateSubmission(form, { email: 'not-an-email' })
    expect(r.ok).toBe(false)
    if (!r.ok) expect(r.error.some((e) => e.field === 'email' && e.code === 'type')).toBe(true)
  })

  it('enforces pattern when provided', () => {
    const patForm: FormDef = { id: 'p', fields: [{ name: 'zip', type: 'text', label: 'Zip', pattern: '^\\d{5}$' }] }
    const bad = validateSubmission(patForm, { zip: 'abc' })
    expect(bad.ok).toBe(false)
    if (!bad.ok) expect(bad.error.some((e) => e.field === 'zip' && e.code === 'pattern')).toBe(true)
    const good = validateSubmission(patForm, { zip: '12345' })
    expect(good.ok).toBe(true)
  })

  it('reads FormData — multiselect via getAll, checkbox via presence', () => {
    const fd = new FormData()
    fd.append('email', 'a@b.co')
    fd.append('tags', 'a')
    fd.append('tags', 'b')
    fd.append('subscribe', 'on')
    const r = validateSubmission(form, fd)
    expect(r.ok).toBe(true)
    if (r.ok) {
      expect(r.value.tags).toEqual(['a', 'b'])
      expect(r.value.subscribe).toBe(true)
    }
  })

  it('checkbox absent in FormData => false (not required)', () => {
    const fd = new FormData()
    fd.append('email', 'a@b.co')
    const r = validateSubmission(form, fd)
    expect(r.ok).toBe(true)
    if (r.ok) expect(r.value.subscribe).toBe(false)
  })

  it('enforces multiselect max count', () => {
    const r = validateSubmission(form, { email: 'a@b.co', tags: ['a', 'b', 'a'] })
    expect(r.ok).toBe(false)
    if (!r.ok) expect(r.error.some((e) => e.field === 'tags' && e.code === 'max')).toBe(true)
  })

  it('SECURITY: an out-of-set option error NEVER echoes the submitted value', () => {
    // pins the security-guard no-echo fix (spec §3): an attacker-controlled option
    // value must not be reflected in the error message (stored-XSS / log-injection floor).
    const sentinel = '<script>alert(1)</script>'
    const sel = validateSubmission(form, { email: 'a@b.co', plan: sentinel })
    expect(sel.ok).toBe(false)
    if (!sel.ok) {
      const e = sel.error.find((x) => x.field === 'plan' && x.code === 'option')
      expect(e).toBeDefined()
      expect(e!.message).not.toContain(sentinel)
    }
    const multi = validateSubmission(form, { email: 'a@b.co', tags: [sentinel] })
    expect(multi.ok).toBe(false)
    if (!multi.ok) {
      const e = multi.error.find((x) => x.field === 'tags' && x.code === 'option')
      expect(e).toBeDefined()
      expect(e!.message).not.toContain(sentinel)
    }
  })

  it('payload-size floor: a free-text field with no explicit max rejects an over-default value', () => {
    const capForm: FormDef = { id: 'cap', fields: [{ name: 'bio', type: 'textarea', label: 'Bio' }] }
    const oversized = 'x'.repeat(100_001)
    const bad = validateSubmission(capForm, { bio: oversized })
    expect(bad.ok).toBe(false)
    if (!bad.ok) expect(bad.error.some((e) => e.field === 'bio' && e.code === 'max')).toBe(true)
    // a normal-length value still passes — the default cap is generous, not restrictive
    const ok = validateSubmission(capForm, { bio: 'hello' })
    expect(ok.ok).toBe(true)
  })
})
