/**
 * test/desktop/settings-store.test.ts — B3: unit tests for the on-disk prefs store.
 *
 * Uses a REAL temp dir under os.tmpdir() (unique per test via process.pid + an
 * incrementing counter — Math.random/Date.now are intentionally avoided). Covers:
 * missing file → defaults (no warn); set→get round-trip; on-disk file contents;
 * corrupt JSON → defaults + warn; and a write failure → error logged, merged
 * result still returned. A spy Logger asserts the logging contract.
 */

import { describe, test, expect, afterEach, vi } from 'vitest'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import type { Logger } from '../../desktop/src/logger.js'
import type { DesktopPrefs } from '../../desktop/src/types.js'
import { createSettingsStore } from '../../desktop/src/settings-store.js'
import { defaultPrefs } from '../../desktop/src/prefs.js'

const PLATFORM: NodeJS.Platform = 'darwin'

// ── unique temp-dir helper (no Math.random / Date.now) ──────────────────────────
let dirCounter = 0
const createdDirs: string[] = []

function makeTempDir(): string {
  dirCounter += 1
  const dir = path.join(os.tmpdir(), `web-terminal-store-test-${process.pid}-${dirCounter}`)
  fs.mkdirSync(dir, { recursive: true })
  createdDirs.push(dir)
  return dir
}

interface SpyLogger extends Logger {
  info: ReturnType<typeof vi.fn>
  warn: ReturnType<typeof vi.fn>
  error: ReturnType<typeof vi.fn>
}

function makeSpyLogger(): SpyLogger {
  return { info: vi.fn(), warn: vi.fn(), error: vi.fn() }
}

afterEach(() => {
  for (const dir of createdDirs) {
    fs.rmSync(dir, { recursive: true, force: true })
  }
  createdDirs.length = 0
})

describe('createSettingsStore.get', () => {
  test('returns defaults when the prefs file does not exist (and logs no warn)', () => {
    // Arrange
    const dir = makeTempDir()
    const logger = makeSpyLogger()
    const store = createSettingsStore(dir, PLATFORM, logger)

    // Act
    const prefs = store.get()

    // Assert — missing file is the normal first-run case, not a warning
    expect(prefs).toEqual(defaultPrefs(PLATFORM))
    expect(logger.warn).not.toHaveBeenCalled()
  })

  test('returns defaults and logs a warn when the file is corrupt JSON', () => {
    // Arrange
    const dir = makeTempDir()
    const logger = makeSpyLogger()
    fs.writeFileSync(path.join(dir, 'prefs.json'), '{ not valid json', 'utf8')
    const store = createSettingsStore(dir, PLATFORM, logger)

    // Act
    const prefs = store.get()

    // Assert
    expect(prefs).toEqual(defaultPrefs(PLATFORM))
    expect(logger.warn).toHaveBeenCalledTimes(1)
  })

  test('validates untrusted on-disk fields, falling back per bad field', () => {
    // Arrange — a hand-edited file with an out-of-range port and wrong-typed flag
    const dir = makeTempDir()
    const logger = makeSpyLogger()
    fs.writeFileSync(
      path.join(dir, 'prefs.json'),
      JSON.stringify({ port: 999999, lanSharing: 'yes', shellPath: '/bin/bash' }),
      'utf8',
    )
    const store = createSettingsStore(dir, PLATFORM, logger)

    // Act
    const prefs = store.get()

    // Assert
    expect(prefs.port).toBeNull()
    expect(prefs.lanSharing).toBe(false)
    expect(prefs.shellPath).toBe('/bin/bash')
  })
})

describe('createSettingsStore.set', () => {
  test('persists the patch and round-trips via a fresh store', () => {
    // Arrange
    const dir = makeTempDir()
    const store = createSettingsStore(dir, PLATFORM, makeSpyLogger())

    // Act
    const merged = store.set({ port: 4321, lanSharing: true })
    const reread = createSettingsStore(dir, PLATFORM, makeSpyLogger()).get()

    // Assert
    expect(merged.port).toBe(4321)
    expect(merged.lanSharing).toBe(true)
    expect(reread).toEqual(merged)
  })

  test('merges over previously-saved prefs rather than replacing them', () => {
    // Arrange
    const dir = makeTempDir()
    const store = createSettingsStore(dir, PLATFORM, makeSpyLogger())
    store.set({ port: 4321, shellPath: '/bin/zsh' })

    // Act — a second partial set keeps the earlier shellPath
    const merged = store.set({ lanSharing: true })

    // Assert
    expect(merged.port).toBe(4321)
    expect(merged.shellPath).toBe('/bin/zsh')
    expect(merged.lanSharing).toBe(true)
  })

  test('writes pretty 2-space JSON to <dir>/prefs.json', () => {
    // Arrange
    const dir = makeTempDir()
    const store = createSettingsStore(dir, PLATFORM, makeSpyLogger())

    // Act
    store.set({ port: 5000 })
    const onDisk = fs.readFileSync(path.join(dir, 'prefs.json'), 'utf8')
    const parsed: unknown = JSON.parse(onDisk)

    // Assert — content matches and the file is indented (pretty-printed)
    expect(parsed).toMatchObject({ port: 5000 })
    expect(onDisk).toContain('\n  "port": 5000')
  })

  test('creates the store directory if it does not yet exist (mkdir -p)', () => {
    // Arrange — point at a not-yet-created nested subdir
    const dir = path.join(makeTempDir(), 'nested', 'deeper')
    const store = createSettingsStore(dir, PLATFORM, makeSpyLogger())

    // Act
    store.set({ port: 6000 })

    // Assert
    expect(fs.existsSync(path.join(dir, 'prefs.json'))).toBe(true)
  })

  test('logs an error but still returns the merged prefs when the write fails', () => {
    // Arrange — make the store "dir" live under a regular FILE, so mkdirSync fails
    const base = makeTempDir()
    const blocker = path.join(base, 'blocker')
    fs.writeFileSync(blocker, 'i am a file, not a directory', 'utf8')
    const unwritableDir = path.join(blocker, 'sub')
    const logger = makeSpyLogger()
    const store = createSettingsStore(unwritableDir, PLATFORM, logger)

    // Act
    const merged = store.set({ port: 7000 })

    // Assert — best-effort: error surfaced, caller still gets the change
    expect(logger.error).toHaveBeenCalledTimes(1)
    expect(merged.port).toBe(7000)
  })

  test('returned merged prefs are a fully-formed DesktopPrefs', () => {
    // Arrange
    const dir = makeTempDir()
    const store = createSettingsStore(dir, PLATFORM, makeSpyLogger())

    // Act
    const merged: DesktopPrefs = store.set({ port: 8000 })

    // Assert — every field present (defaults + patch)
    expect(Object.keys(merged).sort()).toEqual(Object.keys(defaultPrefs(PLATFORM)).sort())
  })
})
