import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
  USER_ID_FILE_NAME,
  getOrCreateAnonymousUserId,
} from '../src/user-id.ts'

const dirs: string[] = []

function tempHome(): string {
  const dir = mkdtempSync(join(tmpdir(), 'dsh-userid-'))
  dirs.push(dir)
  return dir
}

afterEach(() => {
  for (const dir of dirs.splice(0)) {
    chmodSync(dir, 0o700)
    rmSync(dir, { recursive: true, force: true })
  }
})

const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i

describe('getOrCreateAnonymousUserId', () => {
  it('creates, persists, and returns a bare UUID line on first use', () => {
    const home = tempHome()
    const id = getOrCreateAnonymousUserId({ env: { DSH_HOME: home } })
    expect(id).toMatch(UUID)
    expect(readFileSync(join(home, USER_ID_FILE_NAME), 'utf8')).toBe(`${id}\n`)
  })

  it('creates the home directory when missing', () => {
    const home = join(tempHome(), 'nested', 'home')
    const id = getOrCreateAnonymousUserId({ env: { DSH_HOME: home } })
    expect(readFileSync(join(home, USER_ID_FILE_NAME), 'utf8')).toBe(`${id}\n`)
  })

  it('returns the persisted id on subsequent calls, tolerating surrounding whitespace', () => {
    const home = tempHome()
    const existing = '01234567-89ab-4cde-8f01-23456789abcd'
    writeFileSync(join(home, USER_ID_FILE_NAME), `  ${existing}\n\n`, 'utf8')
    expect(getOrCreateAnonymousUserId({ env: { DSH_HOME: home } })).toBe(existing)
  })

  it('overwrites a corrupt file with a fresh id', () => {
    const home = tempHome()
    writeFileSync(join(home, USER_ID_FILE_NAME), 'not-a-uuid\n', 'utf8')
    const id = getOrCreateAnonymousUserId({ env: { DSH_HOME: home } })
    expect(id).toMatch(UUID)
    expect(readFileSync(join(home, USER_ID_FILE_NAME), 'utf8')).toBe(`${id}\n`)
  })

  it('adopts a concurrent winner: exclusive create loses to an id written after the initial read', () => {
    const home = tempHome()
    const winner = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'
    const file = join(home, USER_ID_FILE_NAME)
    // The generator seam runs between the initial read (absent) and the wx
    // write, so planting the winner here simulates the concurrent first launch.
    const id = getOrCreateAnonymousUserId({
      env: { DSH_HOME: home },
      randomUUID: () => {
        writeFileSync(file, `${winner}\n`, 'utf8')
        return 'ffffffff-0000-4000-8000-000000000000'
      },
    })
    expect(id).toBe(winner)
  })

  it('returns a usable id when the home is unwritable, without persisting', () => {
    const home = tempHome()
    const blocked = join(home, 'blocked')
    mkdirSync(blocked)
    chmodSync(blocked, 0o500)
    const id = getOrCreateAnonymousUserId({ env: { DSH_HOME: blocked } })
    expect(id).toMatch(UUID)
    expect(existsSync(join(blocked, USER_ID_FILE_NAME))).toBe(false)
  })

  it('memoizes per resolved home for the process lifetime: one read, deletion-proof', () => {
    const home = tempHome()
    const first = getOrCreateAnonymousUserId({ env: { DSH_HOME: home } })
    rmSync(join(home, USER_ID_FILE_NAME))
    expect(getOrCreateAnonymousUserId({ env: { DSH_HOME: home } })).toBe(first)
  })

  it('keeps distinct homes on distinct ids', () => {
    const a = getOrCreateAnonymousUserId({ env: { DSH_HOME: tempHome() } })
    const b = getOrCreateAnonymousUserId({ env: { DSH_HOME: tempHome() } })
    expect(a).not.toBe(b)
  })

  it('reads process.env by default', () => {
    const home = tempHome()
    const previous = process.env.DSH_HOME
    process.env.DSH_HOME = home
    try {
      const id = getOrCreateAnonymousUserId()
      expect(readFileSync(join(home, USER_ID_FILE_NAME), 'utf8')).toBe(`${id}\n`)
    } finally {
      if (previous === undefined) delete process.env.DSH_HOME
      else process.env.DSH_HOME = previous
    }
  })
})
