import { describe, it, expect } from 'vitest'
import {
  createReceipt,
  formatReceiptLine,
  hashContent,
  normalizeFilePath,
  parseReceiptsJsonl,
} from '../receipts.js'

describe('hashContent', () => {
  it('is deterministic for the same string', () => {
    expect(hashContent('hello')).toBe(hashContent('hello'))
  })

  it('changes when content changes', () => {
    expect(hashContent('a')).not.toBe(hashContent('b'))
  })

  it('hashes Buffers and strings equivalently', () => {
    expect(hashContent('abc')).toBe(hashContent(Buffer.from('abc')))
  })
})

describe('normalizeFilePath', () => {
  it('returns a repo-relative forward-slash path for files under cwd', () => {
    const cwd = '/repo'
    expect(normalizeFilePath('/repo/src/foo.ts', cwd)).toBe('src/foo.ts')
  })

  it('preserves absolute path when file is outside cwd', () => {
    const cwd = '/repo'
    expect(normalizeFilePath('/elsewhere/bar.ts', cwd)).toBe('/elsewhere/bar.ts')
  })

  it('normalizes backslashes to forward slashes', () => {
    const cwd = '/repo'
    // Simulate a Windows-style path relative segment
    expect(normalizeFilePath('/repo/src\\a\\b.ts', cwd)).toBe('src/a/b.ts')
  })
})

describe('createReceipt', () => {
  const base = {
    sessionId: 'sess-1',
    absoluteFilePath: '/repo/src/foo.ts',
    cwd: '/repo',
    now: 1700000000000,
  }

  it('builds a v1 receipt with pre/post hashes', () => {
    const r = createReceipt({
      ...base,
      operation: 'edit',
      contentBefore: 'old',
      contentAfter: 'new',
    })
    expect(r.version).toBe(1)
    expect(r.operation).toBe('edit')
    expect(r.file_path).toBe('src/foo.ts')
    expect(r.hash_before).toBe(hashContent('old'))
    expect(r.hash_after).toBe(hashContent('new'))
    expect(r.bytes_before).toBe(3)
    expect(r.bytes_after).toBe(3)
  })

  it('represents file creation as null hash_before', () => {
    const r = createReceipt({
      ...base,
      operation: 'write',
      contentBefore: null,
      contentAfter: 'hello',
    })
    expect(r.hash_before).toBeNull()
    expect(r.bytes_before).toBe(0)
    expect(r.hash_after).toBe(hashContent('hello'))
  })

  it('represents file deletion as null hash_after', () => {
    const r = createReceipt({
      ...base,
      operation: 'delete',
      contentBefore: 'contents',
      contentAfter: null,
    })
    expect(r.hash_before).toBe(hashContent('contents'))
    expect(r.hash_after).toBeNull()
    expect(r.bytes_after).toBe(0)
  })

  it('includes model when provided', () => {
    const r = createReceipt({
      ...base,
      operation: 'edit',
      contentBefore: 'a',
      contentAfter: 'b',
      model: 'glm-5.1',
    })
    expect(r.model).toBe('glm-5.1')
  })

  it('gives each receipt a unique id', () => {
    const r1 = createReceipt({
      ...base,
      operation: 'edit',
      contentBefore: 'a',
      contentAfter: 'b',
    })
    const r2 = createReceipt({
      ...base,
      operation: 'edit',
      contentBefore: 'a',
      contentAfter: 'c',
    })
    expect(r1.id).not.toBe(r2.id)
  })
})

describe('parseReceiptsJsonl', () => {
  it('parses a valid JSONL blob', () => {
    const r1 = createReceipt({
      sessionId: 's',
      absoluteFilePath: '/repo/a.ts',
      cwd: '/repo',
      operation: 'edit',
      contentBefore: 'x',
      contentAfter: 'y',
      now: 100,
    })
    const blob = JSON.stringify(r1) + '\n' + JSON.stringify(r1) + '\n'
    expect(parseReceiptsJsonl(blob)).toHaveLength(2)
  })

  it('skips blank lines and malformed JSON', () => {
    const blob =
      'not json\n\n{"version":1,"id":"x","timestamp":0,"session_id":"s","file_path":"a","operation":"edit","hash_before":null,"hash_after":null,"bytes_before":0,"bytes_after":0}\n'
    const out = parseReceiptsJsonl(blob)
    expect(out).toHaveLength(1)
  })

  it('rejects entries with non-1 version', () => {
    const blob =
      '{"version":2,"id":"x","timestamp":0,"session_id":"s","file_path":"a","operation":"edit","hash_before":null,"hash_after":null,"bytes_before":0,"bytes_after":0}\n'
    expect(parseReceiptsJsonl(blob)).toHaveLength(0)
  })
})

describe('formatReceiptLine', () => {
  const receipt = createReceipt({
    sessionId: 's',
    absoluteFilePath: '/repo/src/foo.ts',
    cwd: '/repo',
    operation: 'edit',
    contentBefore: 'aaaaaaaaaa',
    contentAfter: 'aaaaaaaaaabbbbb',
    model: 'glm-5.1',
    now: Date.UTC(2026, 3, 17, 10, 30, 0),
  })

  it('contains the file path', () => {
    expect(formatReceiptLine(receipt)).toContain('src/foo.ts')
  })

  it('contains the operation', () => {
    expect(formatReceiptLine(receipt)).toContain('edit')
  })

  it('shows a byte delta', () => {
    expect(formatReceiptLine(receipt)).toContain('+5 bytes')
  })

  it('shows the model in brackets', () => {
    expect(formatReceiptLine(receipt)).toContain('[glm-5.1]')
  })

  it('shows short hashes (8 hex chars each)', () => {
    const line = formatReceiptLine(receipt)
    expect(line).toMatch(/[0-9a-f]{8}\s→\s[0-9a-f]{8}/)
  })

  it('renders an em-dash for null before-hash (file creation)', () => {
    const r = createReceipt({
      sessionId: 's',
      absoluteFilePath: '/repo/src/new.ts',
      cwd: '/repo',
      operation: 'write',
      contentBefore: null,
      contentAfter: 'hi',
      now: 0,
    })
    expect(formatReceiptLine(r)).toContain('—')
  })
})
