// @vitest-environment jsdom
/**
 * test/projects-panel.test.ts — unit tests for pure helpers in public/projects.ts.
 *
 * Covers filterProjects, sortProjects, toggleFav, getFavs/saveFavs.
 * localStorage is provided by the jsdom environment.
 * mountProjects (DOM wiring) is thin glue and not unit-tested here.
 *
 * @xterm/xterm is mocked because projects.ts imports preview-grid.ts which
 * uses Terminal. The mock must be declared before the dynamic import below.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { ProjectInfo } from '../src/types.js'

// ── Stub @xterm/xterm (transitively required via preview-grid.ts) ─────────────
class FakeTerminal {
  open = vi.fn()
  dispose = vi.fn()
}
vi.mock('@xterm/xterm', () => ({ Terminal: FakeTerminal }))

// Dynamic import AFTER mock declaration so the factory is in place.
const { filterProjects, sortProjects, getFavs, saveFavs, toggleFav, normalizeProject } = await import(
  '../public/projects.js'
)

/* ── Helpers ───────────────────────────────────────────────────────────────── */

function makeProject(overrides: Partial<ProjectInfo> = {}): ProjectInfo {
  return {
    name: 'test-repo',
    path: '/home/user/test-repo',
    isGit: true,
    sessions: [],
    ...overrides,
  }
}

/* ── filterProjects ────────────────────────────────────────────────────────── */

describe('filterProjects', () => {
  const projects: ProjectInfo[] = [
    makeProject({ name: 'web-terminal', path: '/home/user/web-terminal' }),
    makeProject({ name: 'api-service', path: '/home/user/projects/api-service' }),
    makeProject({ name: 'utils', path: '/home/user/utils' }),
  ]

  it('returns all projects when query is empty string', () => {
    expect(filterProjects(projects, '')).toHaveLength(3)
  })

  it('returns all projects when query is only whitespace', () => {
    expect(filterProjects(projects, '   ')).toHaveLength(3)
  })

  it('filters by name substring (case-insensitive)', () => {
    const result = filterProjects(projects, 'WEB')
    expect(result).toHaveLength(1)
    expect(result[0]!.name).toBe('web-terminal')
  })

  it('filters by path substring', () => {
    const result = filterProjects(projects, 'projects')
    expect(result).toHaveLength(1)
    expect(result[0]!.name).toBe('api-service')
  })

  it('is case-insensitive on path', () => {
    const result = filterProjects(projects, 'UTILS')
    expect(result).toHaveLength(1)
    expect(result[0]!.name).toBe('utils')
  })

  it('returns empty array when no project matches', () => {
    expect(filterProjects(projects, 'zzz-no-match-at-all')).toHaveLength(0)
  })

  it('does not mutate the input array', () => {
    const original = [...projects]
    filterProjects(projects, 'web')
    expect(projects).toEqual(original)
  })

  it('returns a new array each time', () => {
    const r1 = filterProjects(projects, '')
    const r2 = filterProjects(projects, '')
    expect(r1).not.toBe(r2) // different references
  })
})

/* ── sortProjects ──────────────────────────────────────────────────────────── */

describe('sortProjects', () => {
  const a = makeProject({ name: 'a', path: '/a', lastActiveMs: 1000 })
  const b = makeProject({ name: 'b', path: '/b', lastActiveMs: 3000 })
  const c = makeProject({ name: 'c', path: '/c', lastActiveMs: 2000 })

  it('places favourites before non-favourites', () => {
    const favs = new Set(['/c'])
    const sorted = sortProjects([a, b, c], favs)
    expect(sorted[0]!.path).toBe('/c')
  })

  it('among non-favourites sorts by lastActiveMs descending', () => {
    const sorted = sortProjects([a, b, c], new Set())
    expect(sorted[0]!.path).toBe('/b') // 3000 ms
    expect(sorted[1]!.path).toBe('/c') // 2000 ms
    expect(sorted[2]!.path).toBe('/a') // 1000 ms
  })

  it('among favourites also sorts by lastActiveMs descending', () => {
    const favs = new Set(['/a', '/b'])
    const sorted = sortProjects([a, b, c], favs)
    expect(sorted[0]!.path).toBe('/b') // fav, 3000 ms
    expect(sorted[1]!.path).toBe('/a') // fav, 1000 ms
    expect(sorted[2]!.path).toBe('/c') // non-fav, 2000 ms
  })

  it('handles missing lastActiveMs (treats as 0)', () => {
    const noMs = makeProject({ path: '/x' })
    const withMs = makeProject({ path: '/y', lastActiveMs: 500 })
    const sorted = sortProjects([noMs, withMs], new Set())
    expect(sorted[0]!.path).toBe('/y')
    expect(sorted[1]!.path).toBe('/x')
  })

  it('does not mutate the input array', () => {
    const arr = [a, b, c]
    const origFirst = arr[0]!.path
    sortProjects(arr, new Set())
    expect(arr[0]!.path).toBe(origFirst)
  })

  it('returns a new array', () => {
    const arr = [a, b, c]
    const sorted = sortProjects(arr, new Set())
    expect(sorted).not.toBe(arr)
  })

  it('returns empty array for empty input', () => {
    expect(sortProjects([], new Set())).toEqual([])
  })
})

/* ── toggleFav ─────────────────────────────────────────────────────────────── */

describe('toggleFav', () => {
  it('adds a path that is not present', () => {
    const original = new Set(['/a'])
    const next = toggleFav(original, '/b')
    expect(next.has('/b')).toBe(true)
    expect(next.has('/a')).toBe(true)
  })

  it('removes a path that is already present', () => {
    const original = new Set(['/a', '/b'])
    const next = toggleFav(original, '/a')
    expect(next.has('/a')).toBe(false)
    expect(next.has('/b')).toBe(true)
  })

  it('does not mutate the original set', () => {
    const original = new Set(['/a'])
    toggleFav(original, '/a')
    expect(original.has('/a')).toBe(true) // unchanged
  })

  it('returns a new Set instance', () => {
    const original = new Set(['/a'])
    const next = toggleFav(original, '/b')
    expect(next).not.toBe(original)
  })

  it('works on an empty set (add case)', () => {
    const next = toggleFav(new Set(), '/x')
    expect(next.has('/x')).toBe(true)
    expect(next.size).toBe(1)
  })
})

/* ── getFavs / saveFavs (localStorage persistence) ─────────────────────────── */

describe('getFavs / saveFavs', () => {
  beforeEach(() => {
    localStorage.clear()
  })

  it('getFavs returns empty set when no data is stored', () => {
    expect(getFavs().size).toBe(0)
  })

  it('saveFavs + getFavs round-trips a set of paths', () => {
    const paths = new Set(['/home/user/proj-a', '/home/user/proj-b'])
    saveFavs(paths)
    const loaded = getFavs()
    expect(loaded.has('/home/user/proj-a')).toBe(true)
    expect(loaded.has('/home/user/proj-b')).toBe(true)
    expect(loaded.size).toBe(2)
  })

  it('getFavs returns empty set on malformed JSON', () => {
    localStorage.setItem('web-terminal:proj-favs', 'NOT_JSON{{{')
    expect(getFavs().size).toBe(0)
  })

  it('getFavs returns empty set when stored value is not an array', () => {
    localStorage.setItem('web-terminal:proj-favs', JSON.stringify({ not: 'an-array' }))
    expect(getFavs().size).toBe(0)
  })

  it('getFavs returns empty set when stored null', () => {
    localStorage.setItem('web-terminal:proj-favs', 'null')
    expect(getFavs().size).toBe(0)
  })

  it('saveFavs persists an empty set (removes all favs)', () => {
    saveFavs(new Set(['/a']))
    saveFavs(new Set())
    expect(getFavs().size).toBe(0)
  })

  it('getFavs drops non-string entries from a corrupt/tampered array', () => {
    localStorage.setItem('web-terminal:proj-favs', JSON.stringify(['/a', 42, null, '/b', { x: 1 }]))
    const favs = getFavs()
    expect([...favs].sort()).toEqual(['/a', '/b'])
  })
})

/* ── normalizeProject (untrusted /projects element coercion) ────────────────── */

describe('normalizeProject', () => {
  it('passes a well-formed project through, defaulting sessions to []', () => {
    const p = normalizeProject({ name: 'web', path: '/home/u/web', isGit: true })
    expect(p).toEqual({ name: 'web', path: '/home/u/web', isGit: true, sessions: [] })
  })

  it('preserves a sessions array and optional fields', () => {
    const sess = { id: 's1', status: 'idle', clientCount: 0, createdAt: 1, exited: false }
    const p = normalizeProject({
      name: 'web', path: '/p', isGit: true, branch: 'main', dirty: true, lastActiveMs: 99, sessions: [sess],
    })
    expect(p?.branch).toBe('main')
    expect(p?.dirty).toBe(true)
    expect(p?.lastActiveMs).toBe(99)
    expect(p?.sessions).toHaveLength(1)
  })

  it('coerces a missing sessions field to [] (prevents card-render TypeError)', () => {
    const p = normalizeProject({ name: 'web', path: '/p', isGit: true, sessions: undefined })
    expect(p?.sessions).toEqual([])
  })

  it('returns null for a non-object', () => {
    expect(normalizeProject(null)).toBeNull()
    expect(normalizeProject('x')).toBeNull()
    expect(normalizeProject(42)).toBeNull()
  })

  it('returns null when name or path is missing/non-string', () => {
    expect(normalizeProject({ path: '/p' })).toBeNull()
    expect(normalizeProject({ name: 'web' })).toBeNull()
    expect(normalizeProject({ name: 1, path: '/p' })).toBeNull()
  })

  it('drops optional fields of the wrong type', () => {
    const p = normalizeProject({ name: 'web', path: '/p', branch: 5, dirty: 'yes', lastActiveMs: 'x' })
    expect(p?.branch).toBeUndefined()
    expect(p?.dirty).toBeUndefined()
    expect(p?.lastActiveMs).toBeUndefined()
    expect(p?.isGit).toBe(false)
  })
})
