/**
 * test/desktop/prefs.test.ts — B3: unit tests for prefs defaults/validate/merge.
 *
 * Covers: defaultPrefs shape (and platform-independence); validatePrefs adopting
 * valid fields and falling back per-field on wrong types / out-of-range port /
 * empty shellPath / non-object raw / null / array; that it never throws; and
 * mergePrefs immutability + ignoring undefined patch fields.
 */

import { describe, test, expect } from 'vitest'
import type { DesktopPrefs } from '../../desktop/src/types.js'
import { defaultPrefs, validatePrefs, mergePrefs } from '../../desktop/src/prefs.js'

const EXPECTED_DEFAULTS: DesktopPrefs = {
  port: null,
  lanSharing: false,
  shellPath: null,
  openAtLogin: false,
  notifyOnApproval: true,
  notifyOnStatusChange: true,
  remoteHosts: [],
  selectedHostId: null,
}

describe('defaultPrefs', () => {
  test('returns the documented baseline shape (LAN sharing off by default)', () => {
    // Arrange / Act
    const prefs = defaultPrefs('darwin')

    // Assert
    expect(prefs).toEqual(EXPECTED_DEFAULTS)
  })

  test('is platform-independent (same defaults across platforms)', () => {
    // Arrange / Act
    const darwin = defaultPrefs('darwin')
    const win32 = defaultPrefs('win32')
    const linux = defaultPrefs('linux')

    // Assert
    expect(win32).toEqual(darwin)
    expect(linux).toEqual(darwin)
  })

  test('returns a new object each call (no shared mutable state)', () => {
    // Arrange / Act
    const a = defaultPrefs('darwin')
    const b = defaultPrefs('darwin')

    // Assert
    expect(a).not.toBe(b)
  })
})

describe('validatePrefs', () => {
  test('adopts every field from a fully-valid object', () => {
    // Arrange
    const raw = {
      port: 8080,
      lanSharing: true,
      shellPath: '/bin/bash',
      openAtLogin: true,
      notifyOnApproval: false,
      notifyOnStatusChange: false,
      remoteHosts: [],
      selectedHostId: null,
    }

    // Act
    const prefs = validatePrefs(raw, 'darwin')

    // Assert
    expect(prefs).toEqual(raw)
  })

  test('falls back to defaults when raw is not an object', () => {
    // Assert — strings, numbers, booleans are not prefs objects
    expect(validatePrefs('nope', 'darwin')).toEqual(EXPECTED_DEFAULTS)
    expect(validatePrefs(42, 'darwin')).toEqual(EXPECTED_DEFAULTS)
    expect(validatePrefs(true, 'darwin')).toEqual(EXPECTED_DEFAULTS)
  })

  test('falls back to defaults when raw is null', () => {
    expect(validatePrefs(null, 'darwin')).toEqual(EXPECTED_DEFAULTS)
  })

  test('falls back to defaults when raw is undefined', () => {
    expect(validatePrefs(undefined, 'darwin')).toEqual(EXPECTED_DEFAULTS)
  })

  test('falls back to defaults when raw is an array (arrays are not records)', () => {
    expect(validatePrefs([1, 2, 3], 'darwin')).toEqual(EXPECTED_DEFAULTS)
  })

  test('falls back per-field on wrong types, keeping valid siblings', () => {
    // Arrange — lanSharing is a number (invalid); port/shellPath are valid
    const raw = {
      port: 3001,
      lanSharing: 1,
      shellPath: '/bin/zsh',
      openAtLogin: 'yes',
      notifyOnApproval: null,
      notifyOnStatusChange: 0,
    }

    // Act
    const prefs = validatePrefs(raw, 'darwin')

    // Assert — valid fields adopted, invalid fields fall back to defaults
    expect(prefs.port).toBe(3001)
    expect(prefs.shellPath).toBe('/bin/zsh')
    expect(prefs.lanSharing).toBe(false)
    expect(prefs.openAtLogin).toBe(false)
    expect(prefs.notifyOnApproval).toBe(true)
    expect(prefs.notifyOnStatusChange).toBe(true)
  })

  test('rejects out-of-range and non-integer ports, keeping default null', () => {
    expect(validatePrefs({ port: 0 }, 'darwin').port).toBeNull()
    expect(validatePrefs({ port: 65536 }, 'darwin').port).toBeNull()
    expect(validatePrefs({ port: -1 }, 'darwin').port).toBeNull()
    expect(validatePrefs({ port: 3.5 }, 'darwin').port).toBeNull()
    expect(validatePrefs({ port: NaN }, 'darwin').port).toBeNull()
    expect(validatePrefs({ port: '3000' }, 'darwin').port).toBeNull()
  })

  test('accepts boundary ports (1 and 65535)', () => {
    expect(validatePrefs({ port: 1 }, 'darwin').port).toBe(1)
    expect(validatePrefs({ port: 65535 }, 'darwin').port).toBe(65535)
  })

  test('accepts an explicit null port', () => {
    expect(validatePrefs({ port: null }, 'darwin').port).toBeNull()
  })

  test('rejects empty and whitespace-only shellPath, keeping default null', () => {
    expect(validatePrefs({ shellPath: '' }, 'darwin').shellPath).toBeNull()
    expect(validatePrefs({ shellPath: '   ' }, 'darwin').shellPath).toBeNull()
  })

  test('accepts an explicit null shellPath', () => {
    expect(validatePrefs({ shellPath: null }, 'darwin').shellPath).toBeNull()
  })

  test('rejects a non-string shellPath, keeping default null', () => {
    expect(validatePrefs({ shellPath: 123 }, 'darwin').shellPath).toBeNull()
  })

  test('never throws on hostile input shapes', () => {
    // Assert — a grab-bag of malformed inputs must all return defaults, no throw
    const hostile: unknown[] = [Symbol('x'), () => 0, new Map(), 0n]
    for (const raw of hostile) {
      expect(() => validatePrefs(raw, 'darwin')).not.toThrow()
      expect(validatePrefs(raw, 'darwin')).toEqual(EXPECTED_DEFAULTS)
    }
  })
})

describe('mergePrefs', () => {
  test('applies defined patch fields over the base', () => {
    // Arrange
    const base = defaultPrefs('darwin')

    // Act
    const merged = mergePrefs(base, { port: 4000, lanSharing: true })

    // Assert
    expect(merged.port).toBe(4000)
    expect(merged.lanSharing).toBe(true)
    expect(merged.shellPath).toBeNull()
  })

  test('ignores undefined patch fields (does not clobber base)', () => {
    // Arrange
    const base = mergePrefs(defaultPrefs('darwin'), { port: 5000, shellPath: '/bin/bash' })

    // Act — an explicit-undefined patch field must be ignored
    const merged = mergePrefs(base, { port: undefined, lanSharing: true })

    // Assert
    expect(merged.port).toBe(5000)
    expect(merged.shellPath).toBe('/bin/bash')
    expect(merged.lanSharing).toBe(true)
  })

  test('applies falsy-but-defined patch fields (false / null)', () => {
    // Arrange
    const base = mergePrefs(defaultPrefs('darwin'), { port: 6000, notifyOnApproval: true })

    // Act
    const merged = mergePrefs(base, { port: null, notifyOnApproval: false })

    // Assert — null/false are defined values and must overwrite
    expect(merged.port).toBeNull()
    expect(merged.notifyOnApproval).toBe(false)
  })

  test('does not mutate the base object (immutability)', () => {
    // Arrange
    const base = defaultPrefs('darwin')
    const snapshot = { ...base }

    // Act
    const merged = mergePrefs(base, { port: 7000 })

    // Assert — base untouched, a new object returned
    expect(base).toEqual(snapshot)
    expect(merged).not.toBe(base)
  })

  test('does not mutate the patch object', () => {
    // Arrange
    const base = defaultPrefs('darwin')
    const patch = { port: 8000 }

    // Act
    mergePrefs(base, patch)

    // Assert
    expect(patch).toEqual({ port: 8000 })
  })
})
