import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { PushSubscriptionRecord } from '../../src/types.js'
import {
  isValidSubscriptionRecord,
  loadSubscriptionStore,
} from '../../src/push/subscription-store.js'

const dirs: string[] = []

function tmpFile(name = 'push-subs.json'): string {
  const dir = mkdtempSync(join(tmpdir(), 'webterm-subs-'))
  dirs.push(dir)
  return join(dir, name)
}

function rec(endpoint: string, createdAt = 1000): PushSubscriptionRecord {
  return { endpoint, keys: { p256dh: 'pub-' + endpoint, auth: 'auth-' + endpoint }, createdAt }
}

afterEach(() => {
  for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true })
  vi.restoreAllMocks()
})

describe('isValidSubscriptionRecord', () => {
  it('accepts a well-formed record', () => {
    expect(isValidSubscriptionRecord(rec('https://push/1'))).toBe(true)
  })

  it('rejects non-objects and missing/invalid fields', () => {
    expect(isValidSubscriptionRecord(null)).toBe(false)
    expect(isValidSubscriptionRecord('x')).toBe(false)
    expect(isValidSubscriptionRecord({})).toBe(false)
    expect(isValidSubscriptionRecord({ endpoint: '', keys: { p256dh: 'a', auth: 'b' }, createdAt: 1 })).toBe(false)
    expect(isValidSubscriptionRecord({ endpoint: 'e', keys: { p256dh: 'a' }, createdAt: 1 })).toBe(false)
    expect(isValidSubscriptionRecord({ endpoint: 'e', keys: { p256dh: 1, auth: 'b' }, createdAt: 1 })).toBe(false)
    expect(isValidSubscriptionRecord({ endpoint: 'e', keys: { p256dh: 'a', auth: 'b' } })).toBe(false)
    expect(isValidSubscriptionRecord({ endpoint: 'e', keys: { p256dh: 'a', auth: 'b' }, createdAt: NaN })).toBe(false)
  })
})

describe('loadSubscriptionStore — loading', () => {
  it('starts empty when the file is missing', () => {
    const store = loadSubscriptionStore(tmpFile(), 50)
    expect(store.list()).toEqual([])
  })

  it('starts empty (and logs) on malformed JSON', () => {
    const err = vi.spyOn(console, 'error').mockImplementation(() => {})
    const path = tmpFile()
    writeFileSync(path, '{ not valid json')
    const store = loadSubscriptionStore(path, 50)
    expect(store.list()).toEqual([])
    expect(err).toHaveBeenCalled()
  })

  it('filters out invalid records on load', () => {
    const path = tmpFile()
    writeFileSync(
      path,
      JSON.stringify([rec('https://push/ok'), { endpoint: 123 }, { foo: 'bar' }]),
    )
    const store = loadSubscriptionStore(path, 50)
    expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/ok'])
  })

  it('returns empty when the JSON is not an array', () => {
    const path = tmpFile()
    writeFileSync(path, JSON.stringify({ endpoint: 'x' }))
    expect(loadSubscriptionStore(path, 50).list()).toEqual([])
  })
})

describe('SubscriptionStore — list immutability', () => {
  it('returns a frozen copy that cannot be mutated', () => {
    const store = loadSubscriptionStore(tmpFile(), 50)
    store.add(rec('https://push/1'))
    const snapshot = store.list()
    expect(Object.isFrozen(snapshot)).toBe(true)
    expect(() => (snapshot as PushSubscriptionRecord[]).push(rec('https://push/2'))).toThrow()
    // original store is unaffected
    expect(store.list()).toHaveLength(1)
  })
})

describe('SubscriptionStore — add', () => {
  it('appends valid records', () => {
    const store = loadSubscriptionStore(tmpFile(), 50)
    store.add(rec('https://push/1'))
    store.add(rec('https://push/2'))
    expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/1', 'https://push/2'])
  })

  it('throws on an invalid record (fail-fast at the boundary)', () => {
    const store = loadSubscriptionStore(tmpFile(), 50)
    expect(() => store.add({ endpoint: '' } as unknown as PushSubscriptionRecord)).toThrow()
  })

  it('deduplicates by endpoint, replacing the prior record', () => {
    const store = loadSubscriptionStore(tmpFile(), 50)
    store.add(rec('https://push/1', 1000))
    store.add({ endpoint: 'https://push/1', keys: { p256dh: 'new', auth: 'new' }, createdAt: 2000 })
    const list = store.list()
    expect(list).toHaveLength(1)
    expect(list[0]?.keys.p256dh).toBe('new')
    expect(list[0]?.createdAt).toBe(2000)
  })

  it('enforces a FIFO cap at maxSubs, evicting the oldest', () => {
    const store = loadSubscriptionStore(tmpFile(), 2)
    store.add(rec('https://push/1'))
    store.add(rec('https://push/2'))
    store.add(rec('https://push/3'))
    expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/2', 'https://push/3'])
  })
})

describe('SubscriptionStore — remove / prune', () => {
  it('removes a record by endpoint and is a no-op for unknown endpoints', () => {
    const store = loadSubscriptionStore(tmpFile(), 50)
    store.add(rec('https://push/1'))
    store.add(rec('https://push/2'))
    store.remove('https://push/1')
    store.remove('https://push/absent')
    expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/2'])
  })

  it('prunes a batch of dead endpoints', () => {
    const store = loadSubscriptionStore(tmpFile(), 50)
    store.add(rec('https://push/1'))
    store.add(rec('https://push/2'))
    store.add(rec('https://push/3'))
    store.prune(['https://push/1', 'https://push/3'])
    expect(store.list().map((r) => r.endpoint)).toEqual(['https://push/2'])
  })
})

describe('SubscriptionStore — persist', () => {
  it('writes the file with 0600 permissions and round-trips through load', async () => {
    const path = tmpFile()
    const store = loadSubscriptionStore(path, 50)
    store.add(rec('https://push/1', 11))
    store.add(rec('https://push/2', 22))
    await store.persist()

    const mode = statSync(path).mode & 0o777
    expect(mode).toBe(0o600)

    const onDisk = JSON.parse(readFileSync(path, 'utf8'))
    expect(onDisk.map((r: PushSubscriptionRecord) => r.endpoint)).toEqual([
      'https://push/1',
      'https://push/2',
    ])

    const reloaded = loadSubscriptionStore(path, 50)
    expect(reloaded.list().map((r) => r.endpoint)).toEqual(['https://push/1', 'https://push/2'])
  })

  it('logs but does not throw when the destination is unwritable', async () => {
    const err = vi.spyOn(console, 'error').mockImplementation(() => {})
    // a path whose parent directory does not exist
    const store = loadSubscriptionStore(join(tmpFile(), 'nope', 'subs.json'), 50)
    store.add(rec('https://push/1'))
    await expect(store.persist()).resolves.toBeUndefined()
    expect(err).toHaveBeenCalled()
  })
})
