import type { FormattedPrintable, NamePrintable, TablePrintable } from './output'
import { describe, expect, it } from 'vitest'
import { OutputFormatNotSupportedError } from './errors'
import {
  formatted,
  raw,
  stringifyOutput,
  table,
} from './output'

function makeFormatted(opts: { text?: string, json?: unknown, name?: string }): FormattedPrintable & NamePrintable {
  return {
    text: () => opts.text ?? 'hello',
    json: () => opts.json ?? { msg: 'hello' },
    name: () => opts.name ?? 'my-name',
  }
}

function makeTable(opts: {
  columns?: Array<{ name: string, priority: number }>
  rows?: Array<Array<string | number | boolean | null | undefined>>
  json?: unknown
  name?: string
}): TablePrintable & NamePrintable {
  return {
    tableColumns: () => opts.columns ?? [
      { name: 'NAME', priority: 0 },
      { name: 'STATUS', priority: 0 },
    ],
    tableRows: () => opts.rows ?? [['alice', 'active']],
    json: () => opts.json ?? [{ name: 'alice', status: 'active' }],
    name: () => opts.name ?? 'table-name',
  }
}

describe('raw', () => {
  it('creates RawOutput with kind=raw', () => {
    const out = raw('hello\n')
    expect(out.kind).toBe('raw')
    expect(out.data).toBe('hello\n')
  })
})

describe('table', () => {
  it('creates TableOutput with kind=table', () => {
    const data = makeTable({})
    const out = table({ format: 'json', data })
    expect(out.kind).toBe('table')
    expect(out.format).toBe('json')
    expect(out.data).toBe(data)
  })
})

describe('formatted', () => {
  it('creates FormattedOutput with kind=formatted', () => {
    const data = makeFormatted({})
    const out = formatted({ format: 'text', data })
    expect(out.kind).toBe('formatted')
    expect(out.format).toBe('text')
    expect(out.data).toBe(data)
  })
})

describe('stringifyOutput — raw', () => {
  it('returns raw data as-is', () => {
    expect(stringifyOutput(raw('abc\n'))).toBe('abc\n')
  })
})

describe('stringifyOutput — formatted', () => {
  it('text: calls data.text()', () => {
    const out = formatted({ format: 'text', data: makeFormatted({ text: 'plain text\n' }) })
    expect(stringifyOutput(out)).toBe('plain text\n')
  })

  it('empty format: calls data.text()', () => {
    const out = formatted({ format: '', data: makeFormatted({ text: 'default\n' }) })
    expect(stringifyOutput(out)).toBe('default\n')
  })

  it('json: serializes data.json() with 2-space indent + newline', () => {
    const out = formatted({ format: 'json', data: makeFormatted({ json: { x: 1 } }) })
    expect(stringifyOutput(out)).toBe(`${JSON.stringify({ x: 1 }, null, 2)}\n`)
  })

  it('yaml: renders YAML of data.json()', () => {
    const out = formatted({ format: 'yaml', data: makeFormatted({ json: { x: 1 } }) })
    const result = stringifyOutput(out)
    expect(result).toContain('x: 1')
  })

  it('name: returns data.name() + newline', () => {
    const out = formatted({ format: 'name', data: makeFormatted({ name: 'my-app' }) })
    expect(stringifyOutput(out)).toBe('my-app\n')
  })

  it('name: throws when data has no name()', () => {
    const noName: FormattedPrintable = {
      text: () => 'txt',
      json: () => ({}),
    }
    const out = formatted({ format: 'name', data: noName })
    expect(() => stringifyOutput(out)).toThrow(OutputFormatNotSupportedError)
  })

  it('unknown format: throws with allowed list', () => {
    const out = formatted({ format: 'csv', data: makeFormatted({}) })
    expect(() => stringifyOutput(out)).toThrow(OutputFormatNotSupportedError)
  })
})

describe('stringifyOutput — table', () => {
  it('default format: renders tabular text with header row', () => {
    const out = table({ format: '', data: makeTable({}) })
    const result = stringifyOutput(out)
    expect(result).toContain('NAME')
    expect(result).toContain('STATUS')
    expect(result).toContain('alice')
    expect(result).toContain('active')
  })

  it('wide: includes all columns', () => {
    const data = makeTable({
      columns: [
        { name: 'NAME', priority: 0 },
        { name: 'EXTRA', priority: 1 },
      ],
      rows: [['bob', 'hidden']],
    })
    const wide = table({ format: 'wide', data })
    const narrow = table({ format: '', data })
    expect(stringifyOutput(wide)).toContain('EXTRA')
    expect(stringifyOutput(narrow)).not.toContain('EXTRA')
  })

  it('aligns columns correctly when cells contain CJK double-width characters', () => {
    const data = makeTable({
      columns: [
        { name: 'NAME', priority: 0 },
        { name: 'ID', priority: 0 },
      ],
      rows: [
        ['hello', 'aaa'],
        ['猜谜', 'bbb'], // 猜谜 = 2 CJK chars, display width 4
      ],
    })
    const result = stringifyOutput(table({ format: '', data }))
    const lines = result.split('\n').filter(l => l.length > 0)
    // 'hello' display width 5, '猜谜' display width 4 — column width=5
    // padding after 'hello': 5-5+2=2 spaces → 'hello  aaa'
    // padding after '猜谜':  5-4+2=3 spaces → '猜谜   bbb'
    expect(lines[1]).toBe('hello  aaa')
    expect(lines[2]).toBe('猜谜   bbb')
  })

  it('json: serializes data.json() + newline', () => {
    const out = table({ format: 'json', data: makeTable({ json: [{ id: 1 }] }) })
    expect(stringifyOutput(out)).toBe(`${JSON.stringify([{ id: 1 }], null, 2)}\n`)
  })

  it('yaml: renders YAML of data.json()', () => {
    const out = table({ format: 'yaml', data: makeTable({ json: [{ id: 1 }] }) })
    expect(stringifyOutput(out)).toContain('id: 1')
  })

  it('name: returns data.name() + newline', () => {
    const out = table({ format: 'name', data: makeTable({ name: 'row-name' }) })
    expect(stringifyOutput(out)).toBe('row-name\n')
  })

  it('name: throws when data has no name()', () => {
    const noName: TablePrintable = {
      tableColumns: () => [],
      tableRows: () => [],
      json: () => [],
    }
    const out = table({ format: 'name', data: noName })
    expect(() => stringifyOutput(out)).toThrow(OutputFormatNotSupportedError)
  })

  it('unknown format: throws with allowed list', () => {
    const out = table({ format: 'csv', data: makeTable({}) })
    expect(() => stringifyOutput(out)).toThrow(OutputFormatNotSupportedError)
  })

  it('table renders column padding correctly', () => {
    const data = makeTable({
      columns: [{ name: 'NAME', priority: 0 }, { name: 'ID', priority: 0 }],
      rows: [['alice-longname', '1'], ['bob', '2']],
    })
    const result = stringifyOutput(table({ format: '', data }))
    const lines = result.split('\n').filter(Boolean)
    expect(lines).toHaveLength(3)
    const headerParts = lines[0]!.split(/\s{2,}/)
    expect(headerParts[0]).toBe('NAME')
    expect(headerParts[1]).toBe('ID')
  })

  it('empty columns produces only a newline', () => {
    const data = makeTable({ columns: [], rows: [] })
    expect(stringifyOutput(table({ format: '', data }))).toBe('\n')
  })
})
