import { describe, it, expect, afterEach, vi } from 'vitest'
import { mkdtempSync, rmSync, writeFileSync, readdirSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { readJsonArraySafe, writeJsonAtomic } from '../src/main/jsonStore'
import { logger } from '../src/main/logger'

interface Row {
  id: string
}
const isRow = (o: unknown): o is Row =>
  typeof o === 'object' && o !== null && typeof (o as Row).id === 'string'

describe('jsonStore atomic write + safe read', () => {
  const dirs: string[] = []
  function tmp(): string {
    const d = mkdtempSync(join(tmpdir(), 'aerofetch-jsonstore-'))
    dirs.push(d)
    return d
  }
  afterEach(() => {
    for (const d of dirs.splice(0)) {
      try {
        rmSync(d, { recursive: true, force: true })
      } catch {
        /* ignore */
      }
    }
  })

  it('round-trips a valid array (R1 write → read)', () => {
    const file = join(tmp(), 'data.json')
    writeJsonAtomic(file, [{ id: 'a' }, { id: 'b' }])
    expect(readJsonArraySafe(file, isRow)).toEqual([{ id: 'a' }, { id: 'b' }])
  })

  it('leaves no .tmp file behind (atomic temp+rename)', () => {
    const d = tmp()
    writeJsonAtomic(join(d, 'data.json'), [{ id: 'a' }])
    expect(readdirSync(d)).toEqual(['data.json'])
  })

  it('returns [] for a missing file (first run)', () => {
    expect(readJsonArraySafe(join(tmp(), 'nope.json'), isRow)).toEqual([])
  })

  it('drops rows that fail validation', () => {
    const file = join(tmp(), 'data.json')
    writeFileSync(file, JSON.stringify([{ id: 'a' }, { nope: 1 }, { id: 'c' }]))
    expect(readJsonArraySafe(file, isRow)).toEqual([{ id: 'a' }, { id: 'c' }])
  })

  it('reports success/failure instead of swallowing a bad write (R6)', () => {
    const d = tmp()
    const err = vi.spyOn(logger, 'error').mockImplementation(() => {})
    try {
      // A normal write lands and reports true, with nothing logged.
      expect(writeJsonAtomic(join(d, 'data.json'), [{ id: 'a' }])).toBe(true)
      expect(err).not.toHaveBeenCalled()
      // A target under a non-existent directory can't be written — it must report
      // false (so a caller can warn/retry) and log, not throw or silently drop it.
      expect(writeJsonAtomic(join(d, 'no', 'such', 'dir', 'data.json'), [{ id: 'a' }])).toBe(false)
      expect(err).toHaveBeenCalledOnce()
    } finally {
      err.mockRestore()
    }
    // No stray .tmp left behind by the failed write.
    expect(readdirSync(d)).toEqual(['data.json'])
  })

  it('backs up a corrupt file instead of silently wiping it (R2)', () => {
    const d = tmp()
    const file = join(d, 'data.json')
    writeFileSync(file, '{ truncated, not valid json')
    expect(readJsonArraySafe(file, isRow)).toEqual([])
    const backups = readdirSync(d).filter((f) => f.startsWith('data.json.corrupt-'))
    expect(backups.length).toBe(1)
  })
})
