import { describe, it, expect } from 'vitest'
import {
  CANVAS_VERSION,
  emptyCanvas,
  parseCanvasData,
  isCanvasEmpty,
  countPoints,
  strokeToSvgPath,
  canvasToSvg,
  INK_COLORS,
  STROKE_SIZES,
  type NoteCanvasData,
} from '../canvas'

describe('emptyCanvas', () => {
  it('returns a versioned canvas with no strokes', () => {
    const c = emptyCanvas()
    expect(c.version).toBe(CANVAS_VERSION)
    expect(c.strokes).toEqual([])
  })
})

describe('parseCanvasData', () => {
  it('passes through a valid canvas object', () => {
    const valid: NoteCanvasData = {
      version: CANVAS_VERSION,
      strokes: [{ points: [[1, 2, 0.5], [3, 4, 0.6]] }],
      texts: [],
    }
    expect(parseCanvasData(valid)).toEqual(valid)
  })

  it('falls back to an empty canvas for null / undefined', () => {
    expect(parseCanvasData(null)).toEqual(emptyCanvas())
    expect(parseCanvasData(undefined)).toEqual(emptyCanvas())
  })

  it('falls back to an empty canvas for malformed input', () => {
    expect(parseCanvasData({ foo: 'bar' })).toEqual(emptyCanvas())
    expect(parseCanvasData({ version: 1, strokes: 'nope' })).toEqual(emptyCanvas())
    expect(parseCanvasData('a string')).toEqual(emptyCanvas())
  })

  it('parses a JSON string representation', () => {
    const valid: NoteCanvasData = { version: CANVAS_VERSION, strokes: [{ points: [[0, 0, 0.5]] }], texts: [] }
    expect(parseCanvasData(JSON.stringify(valid))).toEqual(valid)
  })

  it('tolerates 2-tuple points without pressure', () => {
    const parsed = parseCanvasData({ version: 1, strokes: [{ points: [[5, 6]] }] })
    expect(parsed.strokes[0].points[0][0]).toBe(5)
    expect(parsed.strokes[0].points[0][1]).toBe(6)
  })
})

describe('isCanvasEmpty', () => {
  it('is true for an empty canvas and false once a stroke exists', () => {
    expect(isCanvasEmpty(emptyCanvas())).toBe(true)
    expect(isCanvasEmpty({ version: 1, strokes: [{ points: [[0, 0, 0.5]] }] })).toBe(false)
  })

  it('treats strokes with no points as empty', () => {
    expect(isCanvasEmpty({ version: 1, strokes: [{ points: [] }] })).toBe(true)
  })
})

describe('countPoints', () => {
  it('sums points across all strokes', () => {
    expect(countPoints(emptyCanvas())).toBe(0)
    expect(
      countPoints({
        version: 1,
        strokes: [{ points: [[0, 0, 0.5], [1, 1, 0.5]] }, { points: [[2, 2, 0.5]] }],
      }),
    ).toBe(3)
  })
})

describe('strokeToSvgPath', () => {
  it('returns an empty string for an empty stroke', () => {
    expect(strokeToSvgPath([])).toBe('')
  })

  it('returns an SVG path starting with a move command for real points', () => {
    const d = strokeToSvgPath([[10, 10, 0.5], [20, 20, 0.6], [30, 10, 0.7]])
    expect(d.length).toBeGreaterThan(0)
    expect(d.startsWith('M')).toBe(true)
    expect(d.trimEnd().endsWith('Z')).toBe(true)
  })

  it('handles a single-point tap without throwing', () => {
    const d = strokeToSvgPath([[10, 10, 0.5]])
    expect(typeof d).toBe('string')
    expect(d.startsWith('M')).toBe(true)
  })
})

describe('canvasToSvg', () => {
  it('wraps strokes in an svg with the given viewport and one path per stroke', () => {
    const data: NoteCanvasData = {
      version: 1,
      strokes: [{ points: [[0, 0, 0.5], [5, 5, 0.6]] }, { points: [[10, 10, 0.5], [15, 5, 0.6]] }],
    }
    const svg = canvasToSvg(data, { width: 400, height: 300 })
    expect(svg).toContain('<svg')
    expect(svg).toContain('width="400"')
    expect(svg).toContain('height="300"')
    expect(svg).toContain('viewBox="0 0 400 300"')
    expect((svg.match(/<path/g) ?? []).length).toBe(2)
  })

  it('uses currentColor for the fill so callers control the ink color via CSS', () => {
    const svg = canvasToSvg(
      { version: 1, strokes: [{ points: [[0, 0, 0.5], [1, 1, 0.5]] }] },
      { width: 100, height: 100 },
    )
    expect(svg).toContain('fill="currentColor"')
  })

  it('renders an empty (stroke-less) svg for an empty canvas', () => {
    const svg = canvasToSvg(emptyCanvas(), { width: 100, height: 100 })
    expect(svg).toContain('<svg')
    expect((svg.match(/<path/g) ?? []).length).toBe(0)
  })

  it('uses a per-stroke color as fill when present', () => {
    const svg = canvasToSvg(
      { version: 1, strokes: [{ points: [[0, 0, 0.5], [1, 1, 0.5]], color: '#D93025' }] },
      { width: 100, height: 100 },
    )
    expect(svg).toContain('fill="#D93025"')
  })

  it('honors a per-stroke size (different geometry than the default)', () => {
    const points: number[][] = [[0, 0, 0.5], [10, 10, 0.6], [20, 0, 0.7]]
    const thin = canvasToSvg({ version: 1, strokes: [{ points, size: 2 }] }, { width: 100, height: 100 })
    const thick = canvasToSvg({ version: 1, strokes: [{ points, size: 20 }] }, { width: 100, height: 100 })
    expect(thin).not.toBe(thick)
  })
})

describe('pen presets', () => {
  it('exposes ink colors and stroke sizes with stable shapes', () => {
    expect(INK_COLORS.length).toBeGreaterThanOrEqual(3)
    for (const c of INK_COLORS) {
      expect(typeof c.label).toBe('string')
      expect(c.value).toMatch(/^#[0-9a-fA-F]{6}$/)
    }
    expect(STROKE_SIZES.length).toBeGreaterThanOrEqual(3)
    for (const s of STROKE_SIZES) {
      expect(typeof s.label).toBe('string')
      expect(s.value).toBeGreaterThan(0)
    }
  })

  it('never uses the reserved logo blue as an ink color (logo-only)', () => {
    // Built by concat so this test file itself stays clean of the literal hex
    // that check:hardcoded-colors forbids.
    const roundel = ('#0066' + 'B1').toUpperCase()
    for (const c of INK_COLORS) expect(c.value.toUpperCase()).not.toBe(roundel)
  })
})

describe('parseCanvasData with style', () => {
  it('preserves per-stroke color and size', () => {
    const data: NoteCanvasData = {
      version: 1,
      strokes: [{ points: [[0, 0, 0.5]], color: '#037493', size: 12 }],
    }
    const parsed = parseCanvasData(data)
    expect(parsed.strokes[0].color).toBe('#037493')
    expect(parsed.strokes[0].size).toBe(12)
  })

  it('still accepts legacy strokes without color/size', () => {
    const parsed = parseCanvasData({ version: 1, strokes: [{ points: [[0, 0, 0.5]] }] })
    expect(parsed.strokes[0].color).toBeUndefined()
    expect(parsed.strokes[0].size).toBeUndefined()
  })
})

describe('parseCanvasData with text labels', () => {
  it('preserves text elements', () => {
    const parsed = parseCanvasData({
      version: 1,
      strokes: [],
      texts: [{ x: 100, y: 200, text: 'Wertstrom', color: '#D93025', size: 28 }],
    })
    expect(parsed.texts).toHaveLength(1)
    expect(parsed.texts?.[0]).toEqual({ x: 100, y: 200, text: 'Wertstrom', color: '#D93025', size: 28 })
  })

  it('defaults texts to an empty array for legacy canvases', () => {
    const parsed = parseCanvasData({ version: 1, strokes: [] })
    expect(parsed.texts).toEqual([])
  })

  it('drops malformed text entries (whole canvas degrades safely)', () => {
    const parsed = parseCanvasData({ version: 1, strokes: [], texts: [{ x: 'nope', text: 5 }] })
    // malformed → schema fails → empty canvas (texts still an array)
    expect(Array.isArray(parsed.texts)).toBe(true)
  })
})
