import { describe, it, expect, beforeEach, vi } from 'vitest'

// icons.ts reads the reactive `apps` store (derived from built-ins +
// installed apps) to seed default positions. Stub it as a minimal Readable
// emitting a fixed list rather than pull in the real registry.
vi.mock('$lib/apps', () => {
  const list = [
    { id: 'tasks' },
    { id: 'kb' },
    { id: 'ops' },
    { id: 'signals' },
    { id: 'knowledge' },
    { id: 'learning' }
  ]
  return {
    apps: {
      subscribe: (cb: (v: typeof list) => void) => {
        cb(list)
        return () => {}
      }
    }
  }
})

import {
  GRID,
  iconPositions,
  placeIcon,
  iconPixelPos,
  maxCols,
  getIconPositions,
  resetIconLayout
} from './icons'

// placeIcon mutates the shared module-level store, so each test starts from
// a known, empty layout rather than whatever the previous test (or apps.ts's
// registry-derived defaults) left behind.
beforeEach(() => {
  iconPositions.set({})
  localStorage.clear()
})

describe('iconPixelPos', () => {
  it('converts a grid cell to pixel coordinates using GRID constants', () => {
    expect(iconPixelPos({ col: 0, row: 0 })).toEqual({ x: GRID.padding, y: GRID.padding })
    expect(iconPixelPos({ col: 1, row: 2 })).toEqual({
      x: GRID.padding + (GRID.cell + GRID.gap),
      y: GRID.padding + 2 * (GRID.cell + GRID.gap)
    })
  })
})

describe('maxCols', () => {
  it('computes how many columns fit in a viewport width', () => {
    const cellSpan = GRID.cell + GRID.gap
    expect(maxCols(GRID.padding + cellSpan * 3)).toBe(3)
  })

  it('never returns less than 1, even for a tiny viewport', () => {
    expect(maxCols(0)).toBe(1)
    expect(maxCols(GRID.padding)).toBe(1)
  })
})

describe('placeIcon', () => {
  it('places an icon at the requested cell when it is free', () => {
    placeIcon('tasks', 3, 4)
    expect(getIconPositions().tasks).toEqual({ col: 3, row: 4 })
  })

  it('clamps negative coordinates to 0 for an otherwise-free cell', () => {
    placeIcon('tasks', -5, -2)
    expect(getIconPositions().tasks).toEqual({ col: 0, row: 0 })
  })

  it('nudges to the nearest free cell when the target is occupied', () => {
    iconPositions.set({ kb: { col: 2, row: 2 } })
    placeIcon('tasks', 2, 2)
    const pos = getIconPositions().tasks
    // Must not land on top of kb, and must be one of the 8 immediate
    // neighbors (radius-1 ring) since all of them are free.
    expect(pos).not.toEqual({ col: 2, row: 2 })
    expect(Math.max(Math.abs(pos.col - 2), Math.abs(pos.row - 2))).toBe(1)
  })

  it('does not disturb the icon already occupying a cell when another icon is nudged past it', () => {
    iconPositions.set({ kb: { col: 2, row: 2 } })
    placeIcon('tasks', 2, 2)
    expect(getIconPositions().kb).toEqual({ col: 2, row: 2 })
  })

  it('moving an icon back onto its own current cell is a no-op collision (never nudges against itself)', () => {
    iconPositions.set({ tasks: { col: 5, row: 5 } })
    placeIcon('tasks', 5, 5)
    expect(getIconPositions().tasks).toEqual({ col: 5, row: 5 })
  })

  it('persists the updated layout to localStorage', () => {
    placeIcon('tasks', 1, 1)
    const stored = JSON.parse(localStorage.getItem('oikos-desktop-icons') ?? '{}')
    expect(stored.tasks).toEqual({ col: 1, row: 1 })
  })
})

describe('resetIconLayout', () => {
  it('restores the classic left-edge column in registry order', () => {
    iconPositions.set({ tasks: { col: 4, row: 7 }, kb: { col: 1, row: 1 } })
    resetIconLayout()
    expect(getIconPositions()).toEqual({
      tasks: { col: 0, row: 0 },
      kb: { col: 0, row: 1 },
      ops: { col: 0, row: 2 },
      signals: { col: 0, row: 3 },
      knowledge: { col: 0, row: 4 },
      learning: { col: 0, row: 5 }
    })
  })
})
