/**
 * Tests for usePixelWordPositions hook.
 *
 * The hook performs pixel-based word positioning using an offscreen canvas.
 * Since Canvas/getImageData is not available in jsdom, we test the pure
 * computation logic by extracting and testing the algorithms directly.
 */
import { describe, it, expect } from 'vitest'


// ---------------------------------------------------------------------------
// Extract pure computation functions from the hook for testing
// ---------------------------------------------------------------------------

interface Cluster {
  start: number
  end: number
}

/**
 * Cluster detection: find runs of dark pixels above a threshold.
 * Replicates the cluster detection logic in usePixelWordPositions.
 */
function findClusters(proj: number[], ch: number, cw: number): Cluster[] {
  const threshold = Math.max(1, ch * 0.03)
  const minGap = Math.max(5, Math.round(cw * 0.02))
  const clusters: Cluster[] = []
  let inCluster = false
  let clStart = 0
  let gap = 0

  for (let x = 0; x < cw; x++) {
    if (proj[x] >= threshold) {
      if (!inCluster) { clStart = x; inCluster = true }
      gap = 0
    } else if (inCluster) {
      gap++
      if (gap > minGap) {
        clusters.push({ start: clStart, end: x - gap })
        inCluster = false
        gap = 0
      }
    }
  }
  if (inCluster) clusters.push({ start: clStart, end: cw - 1 - gap })

  return clusters
}

/**
 * Mirror clusters for 180° rotation.
 * Replicates the rotation logic in usePixelWordPositions.
 */
function mirrorClusters(clusters: Cluster[], cw: number): Cluster[] {
  return clusters.map(c => ({
    start: cw - 1 - c.end,
    end: cw - 1 - c.start,
  })).reverse()
}

/**
 * Compute fontRatio from cluster width, measured text width, and cell height.
 * Replicates the font ratio calculation.
 */
function computeFontRatio(
  clusterW: number,
  measuredWidth: number,
  refFontSize: number,
  ch: number,
): number {
  const autoFontPx = refFontSize * (clusterW / measuredWidth)
  return Math.min(autoFontPx / ch, 1.0)
}

/**
 * Mode normalization: find the most common fontRatio (bucketed to 0.02).
 * Replicates the mode normalization in usePixelWordPositions.
 */
function normalizeFontRatios(ratios: number[]): number {
  if (ratios.length === 0) return 0
  const buckets = new Map<number, number>()
  for (const r of ratios) {
    const key = Math.round(r * 50) / 50
    buckets.set(key, (buckets.get(key) || 0) + 1)
  }
  let modeRatio = ratios[0]
  let modeCount = 0
  for (const [ratio, count] of buckets) {
    if (count > modeCount) { modeRatio = ratio; modeCount = count }
  }
  return modeRatio
}

/**
 * Coordinate transform for 180° rotation.
 */
function transformCellCoords180(
  x: number, y: number, w: number, h: number,
  imgW: number, imgH: number,
): { cx: number; cy: number } {
  return {
    cx: Math.round((100 - x - w) / 100 * imgW),
    cy: Math.round((100 - y - h) / 100 * imgH),
  }
}


// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('findClusters', () => {
  it('should find a single cluster', () => {
    // Simulate a projection with dark pixels from x=10 to x=50
    const proj = new Array(100).fill(0)
    for (let x = 10; x <= 50; x++) proj[x] = 10

    const clusters = findClusters(proj, 100, 100)
    expect(clusters.length).toBe(1)
    expect(clusters[0].start).toBe(10)
    expect(clusters[0].end).toBe(50)
  })

  it('should find multiple clusters separated by gaps', () => {
    const proj = new Array(200).fill(0)
    // Two word groups with a gap between
    for (let x = 10; x <= 40; x++) proj[x] = 10
    for (let x = 80; x <= 120; x++) proj[x] = 10

    const clusters = findClusters(proj, 100, 200)
    expect(clusters.length).toBe(2)
    expect(clusters[0].start).toBe(10)
    expect(clusters[1].start).toBe(80)
  })

  it('should merge clusters with small gaps', () => {
    // Gap smaller than minGap should not split clusters
    const proj = new Array(100).fill(0)
    for (let x = 10; x <= 30; x++) proj[x] = 10
    // Small gap (3px) — minGap = max(5, 100*0.02) = 5
    for (let x = 34; x <= 50; x++) proj[x] = 10

    const clusters = findClusters(proj, 100, 100)
    expect(clusters.length).toBe(1)  // merged into one cluster
  })

  it('should return empty for all-white projection', () => {
    const proj = new Array(100).fill(0)
    const clusters = findClusters(proj, 100, 100)
    expect(clusters.length).toBe(0)
  })
})


describe('mirrorClusters', () => {
  it('should mirror clusters for 180° rotation', () => {
    const clusters: Cluster[] = [
      { start: 10, end: 50 },
      { start: 80, end: 120 },
    ]
    const cw = 200

    const mirrored = mirrorClusters(clusters, cw)

    // Cluster at (10,50) → (cw-1-50, cw-1-10) = (149, 189)
    // Cluster at (80,120) → (cw-1-120, cw-1-80) = (79, 119)
    // After reverse: [(79,119), (149,189)]
    expect(mirrored.length).toBe(2)
    expect(mirrored[0]).toEqual({ start: 79, end: 119 })
    expect(mirrored[1]).toEqual({ start: 149, end: 189 })
  })

  it('should maintain left-to-right order after mirroring', () => {
    const clusters: Cluster[] = [
      { start: 5, end: 30 },
      { start: 50, end: 80 },
      { start: 100, end: 130 },
    ]

    const mirrored = mirrorClusters(clusters, 200)

    // After mirroring and reversing, order should be left-to-right
    for (let i = 1; i < mirrored.length; i++) {
      expect(mirrored[i].start).toBeGreaterThan(mirrored[i - 1].start)
    }
  })

  it('should handle single cluster', () => {
    const clusters: Cluster[] = [{ start: 20, end: 80 }]
    const mirrored = mirrorClusters(clusters, 200)

    expect(mirrored.length).toBe(1)
    expect(mirrored[0]).toEqual({ start: 119, end: 179 })
  })
})


describe('computeFontRatio', () => {
  it('should compute ratio based on cluster vs measured width', () => {
    // Cluster is 100px wide, measured text at 40px font is 200px → autoFont = 20px
    // Cell height = 30px → ratio = 20/30 = 0.667
    const ratio = computeFontRatio(100, 200, 40, 30)
    expect(ratio).toBeCloseTo(0.667, 2)
  })

  it('should cap ratio at 1.0', () => {
    // Very large cluster relative to measured text
    const ratio = computeFontRatio(400, 100, 40, 30)
    expect(ratio).toBe(1.0)
  })

  it('should handle small cluster width', () => {
    const ratio = computeFontRatio(10, 200, 40, 30)
    expect(ratio).toBeCloseTo(0.067, 2)
  })
})


describe('normalizeFontRatios', () => {
  it('should return the most common ratio', () => {
    const ratios = [0.5, 0.5, 0.5, 0.3, 0.3, 0.7]
    const mode = normalizeFontRatios(ratios)
    expect(mode).toBe(0.5)
  })

  it('should bucket ratios to nearest 0.02', () => {
    // 0.51 and 0.49 both round to 0.50 (nearest 0.02)
    const ratios = [0.51, 0.49, 0.50, 0.30]
    const mode = normalizeFontRatios(ratios)
    expect(mode).toBe(0.50)
  })

  it('should handle empty array', () => {
    expect(normalizeFontRatios([])).toBe(0)
  })

  it('should handle single ratio', () => {
    expect(normalizeFontRatios([0.65])).toBe(0.66)  // rounded to nearest 0.02
  })
})


describe('transformCellCoords180', () => {
  it('should transform cell coordinates for 180° rotation', () => {
    // Cell at x=10%, y=20%, w=30%, h=5% on a 1000x2000 image
    const { cx, cy } = transformCellCoords180(10, 20, 30, 5, 1000, 2000)

    // Expected: cx = (100 - 10 - 30) / 100 * 1000 = 600
    //           cy = (100 - 20 - 5) / 100 * 2000 = 1500
    expect(cx).toBe(600)
    expect(cy).toBe(1500)
  })

  it('should handle cell at origin', () => {
    const { cx, cy } = transformCellCoords180(0, 0, 50, 50, 1000, 1000)

    // Expected: cx = (100 - 0 - 50) / 100 * 1000 = 500
    //           cy = (100 - 0 - 50) / 100 * 1000 = 500
    expect(cx).toBe(500)
    expect(cy).toBe(500)
  })

  it('should handle cell at bottom-right', () => {
    const { cx, cy } = transformCellCoords180(80, 90, 20, 10, 1000, 2000)

    // Expected: cx = (100 - 80 - 20) / 100 * 1000 = 0
    //           cy = (100 - 90 - 10) / 100 * 2000 = 0
    expect(cx).toBe(0)
    expect(cy).toBe(0)
  })
})


describe('sub-session coordinate conversion', () => {
  /**
   * Test the coordinate conversion from sub-session (box-relative)
   * to parent (page-absolute) coordinates.
   * Replicates the logic in StepReconstruction loadSessionData.
   */
  it('should convert sub-session cell coords to parent space', () => {
    const imgW = 1746
    const imgH = 2487

    // Box zone in pixels
    const box = { x: 50, y: 1145, width: 1100, height: 270 }

    // Box in percent
    const boxXPct = (box.x / imgW) * 100
    const boxYPct = (box.y / imgH) * 100
    const boxWPct = (box.width / imgW) * 100
    const boxHPct = (box.height / imgH) * 100

    // Sub-session cell at (10%, 20%, 80%, 15%) relative to box
    const subCell = { x: 10, y: 20, w: 80, h: 15 }

    const parentX = boxXPct + (subCell.x / 100) * boxWPct
    const parentY = boxYPct + (subCell.y / 100) * boxHPct
    const parentW = (subCell.w / 100) * boxWPct
    const parentH = (subCell.h / 100) * boxHPct

    // Box start in percent: x ≈ 2.86%, y ≈ 46.04%
    expect(parentX).toBeCloseTo(boxXPct + 0.1 * boxWPct, 2)
    expect(parentY).toBeCloseTo(boxYPct + 0.2 * boxHPct, 2)
    expect(parentW).toBeCloseTo(0.8 * boxWPct, 2)
    expect(parentH).toBeCloseTo(0.15 * boxHPct, 2)

    // All values should be within 0-100%
    expect(parentX).toBeGreaterThan(0)
    expect(parentY).toBeGreaterThan(0)
    expect(parentX + parentW).toBeLessThan(100)
    expect(parentY + parentH).toBeLessThan(100)
  })

  it('should place sub-cell at box origin when sub coords are 0,0', () => {
    const imgW = 1000
    const imgH = 2000
    const box = { x: 100, y: 500, width: 800, height: 200 }

    const boxXPct = (box.x / imgW) * 100  // 10%
    const boxYPct = (box.y / imgH) * 100  // 25%

    const parentX = boxXPct + (0 / 100) * ((box.width / imgW) * 100)
    const parentY = boxYPct + (0 / 100) * ((box.height / imgH) * 100)

    expect(parentX).toBeCloseTo(10, 1)
    expect(parentY).toBeCloseTo(25, 1)
  })
})
