import { describe, it, expect } from 'vitest'
import {
  mergeQafComparisonUserInputs,
  normalizeQafComparisonUserInputsPatch,
  readQafProjectionUserInputs,
  readQafCalculatorUserInputs,
  validateQafComparisonUserInputsPatch,
  type QafProjectionUserInputs,
  type QafCalculatorUserInputs,
} from '../comparison-user-inputs'
import { EMPTY_UNIT_PRICE_PARAMS } from '../g60/calculator'

const PROJECTION: QafProjectionUserInputs = {
  years: [{ year: 2026, volume: 1000, included: true }],
  overrideDelta: 1.5,
  defendPct: 0.3,
}
const CALCULATOR: QafCalculatorUserInputs = { params: { ...EMPTY_UNIT_PRICE_PARAMS, cycleSec: 12 } }

describe('mergeQafComparisonUserInputs', () => {
  it('merges a projection patch into an empty/null current value', () => {
    expect(mergeQafComparisonUserInputs(null, { projection: PROJECTION })).toEqual({ v: 1, projection: PROJECTION })
    expect(mergeQafComparisonUserInputs({}, { projection: PROJECTION })).toEqual({ v: 1, projection: PROJECTION })
  })

  it('a projection save never clobbers an already-saved calculator value (patch semantics)', () => {
    const current = { v: 1, calculator: CALCULATOR }
    const next = mergeQafComparisonUserInputs(current, { projection: PROJECTION })
    expect(next.calculator).toEqual(CALCULATOR)
    expect(next.projection).toEqual(PROJECTION)
  })

  it('a calculator save never clobbers an already-saved projection value (patch semantics)', () => {
    const current = { v: 1, projection: PROJECTION }
    const next = mergeQafComparisonUserInputs(current, { calculator: CALCULATOR })
    expect(next.projection).toEqual(PROJECTION)
    expect(next.calculator).toEqual(CALCULATOR)
  })

  it('replaces its own namespace wholesale — a second save on the same namespace does not deep-merge with the first', () => {
    const current = { v: 1, projection: PROJECTION }
    const secondSave: QafProjectionUserInputs = { years: [], overrideDelta: null, defendPct: 0 }
    const next = mergeQafComparisonUserInputs(current, { projection: secondSave })
    expect(next.projection).toEqual(secondSave)
  })

  it('treats a malformed (non-object) current value as empty rather than throwing', () => {
    expect(mergeQafComparisonUserInputs('garbage', { calculator: CALCULATOR })).toEqual({ v: 1, calculator: CALCULATOR })
    expect(mergeQafComparisonUserInputs(42, { calculator: CALCULATOR })).toEqual({ v: 1, calculator: CALCULATOR })
    expect(mergeQafComparisonUserInputs(undefined, { calculator: CALCULATOR })).toEqual({ v: 1, calculator: CALCULATOR })
  })

  it('always stamps v: 1', () => {
    expect(mergeQafComparisonUserInputs({ v: 1 }, { projection: PROJECTION }).v).toBe(1)
  })
})

describe('readQafProjectionUserInputs / readQafCalculatorUserInputs', () => {
  it('reads back the persisted namespace when present', () => {
    const bag = { v: 1, projection: PROJECTION, calculator: CALCULATOR }
    expect(readQafProjectionUserInputs(bag)).toEqual(PROJECTION)
    expect(readQafCalculatorUserInputs(bag)).toEqual(CALCULATOR)
  })

  it('returns null (never fabricated) when the namespace key is absent', () => {
    expect(readQafProjectionUserInputs({ v: 1, calculator: CALCULATOR })).toBeNull()
    expect(readQafCalculatorUserInputs({ v: 1, projection: PROJECTION })).toBeNull()
  })

  it('returns null for an empty bag (default column value, nothing saved yet)', () => {
    expect(readQafProjectionUserInputs({})).toBeNull()
    expect(readQafCalculatorUserInputs({})).toBeNull()
  })

  it('returns null for null/undefined/malformed bags — the pre-migration / load-error state', () => {
    expect(readQafProjectionUserInputs(null)).toBeNull()
    expect(readQafProjectionUserInputs(undefined)).toBeNull()
    expect(readQafProjectionUserInputs('not-an-object')).toBeNull()
    expect(readQafCalculatorUserInputs(null)).toBeNull()
    expect(readQafCalculatorUserInputs(123)).toBeNull()
  })
})

describe('validateQafComparisonUserInputsPatch', () => {
  it('accepts a valid projection-only patch', () => {
    expect(validateQafComparisonUserInputsPatch({ projection: PROJECTION })).toBeNull()
  })

  it('accepts a valid calculator-only patch', () => {
    expect(validateQafComparisonUserInputsPatch({ calculator: CALCULATOR })).toBeNull()
  })

  it('rejects a non-object patch', () => {
    expect(validateQafComparisonUserInputsPatch(null)).not.toBeNull()
    expect(validateQafComparisonUserInputsPatch('x')).not.toBeNull()
    expect(validateQafComparisonUserInputsPatch(42)).not.toBeNull()
  })

  it('rejects an empty patch — nothing to save', () => {
    expect(validateQafComparisonUserInputsPatch({})).not.toBeNull()
  })

  it('rejects an unknown top-level key', () => {
    expect(validateQafComparisonUserInputsPatch({ notARealNamespace: {} })).not.toBeNull()
  })

  it('rejects a projection payload with non-array years', () => {
    expect(validateQafComparisonUserInputsPatch({ projection: { ...PROJECTION, years: 'nope' } })).not.toBeNull()
  })

  it('rejects a projection payload with more than 50 year rows', () => {
    const years = Array.from({ length: 51 }, (_, i) => ({ year: 2000 + i, volume: 0, included: true }))
    expect(validateQafComparisonUserInputsPatch({ projection: { ...PROJECTION, years } })).not.toBeNull()
  })

  it('rejects a malformed year row', () => {
    expect(
      validateQafComparisonUserInputsPatch({ projection: { ...PROJECTION, years: [{ year: 'not-a-number', volume: 0, included: true }] } }),
    ).not.toBeNull()
    expect(
      validateQafComparisonUserInputsPatch({ projection: { ...PROJECTION, years: [{ year: 2026, volume: 0, included: 'yes' }] } }),
    ).not.toBeNull()
  })

  it('rejects a non-finite overrideDelta', () => {
    expect(validateQafComparisonUserInputsPatch({ projection: { ...PROJECTION, overrideDelta: Number.NaN } })).not.toBeNull()
    expect(validateQafComparisonUserInputsPatch({ projection: { ...PROJECTION, overrideDelta: Infinity } })).not.toBeNull()
  })

  it('accepts a null overrideDelta (no override set)', () => {
    expect(validateQafComparisonUserInputsPatch({ projection: { ...PROJECTION, overrideDelta: null } })).toBeNull()
  })

  it('rejects a defendPct outside [0, 1]', () => {
    expect(validateQafComparisonUserInputsPatch({ projection: { ...PROJECTION, defendPct: -0.1 } })).not.toBeNull()
    expect(validateQafComparisonUserInputsPatch({ projection: { ...PROJECTION, defendPct: 1.1 } })).not.toBeNull()
  })

  it('rejects a calculator payload missing a required param', () => {
    const incomplete: Record<string, number> = { ...CALCULATOR.params }
    delete incomplete.cycleSec
    expect(validateQafComparisonUserInputsPatch({ calculator: { params: incomplete } })).not.toBeNull()
  })

  it('rejects a calculator payload with a non-finite param', () => {
    expect(
      validateQafComparisonUserInputsPatch({ calculator: { params: { ...CALCULATOR.params, materialPerUnit: Number.NaN } } }),
    ).not.toBeNull()
  })
})

describe('normalizeQafComparisonUserInputsPatch (PR #349 review fix)', () => {
  it('strips unknown extra properties from projection payloads (top level and per year row)', () => {
    const dirty = {
      projection: {
        years: [{ year: 2026, volume: 1000, included: true, smuggled: 'x'.repeat(10_000) }],
        overrideDelta: null,
        defendPct: 0.2,
        extraBlob: { huge: 'y'.repeat(10_000) },
      },
    } as unknown as Parameters<typeof normalizeQafComparisonUserInputsPatch>[0]
    expect(validateQafComparisonUserInputsPatch(dirty)).toBeNull() // validation alone lets this through
    const clean = normalizeQafComparisonUserInputsPatch(dirty)
    expect(clean).toEqual({
      projection: {
        years: [{ year: 2026, volume: 1000, included: true }],
        overrideDelta: null,
        defendPct: 0.2,
      },
    })
  })

  it('strips unknown extra properties from calculator payloads, keeping exactly the known params', () => {
    const dirty = {
      calculator: {
        params: { ...CALCULATOR.params, smuggledParam: 999 },
        extra: 'nope',
      },
    } as unknown as Parameters<typeof normalizeQafComparisonUserInputsPatch>[0]
    expect(validateQafComparisonUserInputsPatch(dirty)).toBeNull()
    const clean = normalizeQafComparisonUserInputsPatch(dirty)
    expect(clean).toEqual({ calculator: { params: CALCULATOR.params } })
  })
})
