// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, render, screen, fireEvent, waitFor } from '@testing-library/react'

// Keep the pure shaping helpers (applyPagesQuery/groupPages) real — the
// list view relies on them for in-memory filtering — and stub only the
// network read + bulk mutations.
const fetchPagesDataset = vi.fn()
vi.mock('../../lib/pages-service.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../lib/pages-service.js')>()
  return {
    ...actual,
    fetchPagesDataset: () => fetchPagesDataset(),
  }
})

// This happy-dom environment doesn't expose a functional localStorage, which
// the view reads for the persisted List/Tree mode.
const storage = new Map<string, string>()
vi.stubGlobal('localStorage', {
  getItem: (k: string) => storage.get(k) ?? null,
  setItem: (k: string, v: string) => void storage.set(k, String(v)),
  removeItem: (k: string) => void storage.delete(k),
  clear: () => storage.clear(),
})

const { PagesListView } = await import('../../views/Pages/PagesListView.js')
import type { Page, PageTag, PagesDataset } from '../../lib/pages-service.js'

afterEach(() => {
  cleanup()
  fetchPagesDataset.mockReset()
  // The view persists the List/Tree choice — clear it so tests stay isolated.
  storage.clear()
})

function page(id: string, over: Partial<Page> = {}): Page {
  return {
    id,
    title: id,
    path: `/${id}`,
    slug: id,
    parentPageId: null,
    tags: [],
    primaryTagId: null,
    authorId: 'u1',
    authorName: 'Jane Doe',
    authorInitials: 'JD',
    status: 'PUBLISHED',
    publishDate: null,
    scheduledDate: null,
    lastModified: '2026-01-01T00:00:00Z',
    seoScore: 80,
    seoTitle: null,
    seoDescription: null,
    templateId: null,
    isHomepage: false,
    isSystemPage: false,
    createdAt: '2026-01-01T00:00:00Z',
    updatedAt: '2026-01-01T00:00:00Z',
    ...over,
  }
}

function tag(slug: string, pageCount: number): PageTag {
  return {
    id: slug,
    docId: `doc-${slug}`,
    name: slug.toUpperCase(),
    slug,
    description: null,
    color: 'green',
    pageCount,
    sortOrder: 0,
    active: true,
    createdAt: null,
    updatedAt: null,
  }
}

function dataset(over: Partial<PagesDataset> = {}): PagesDataset {
  return { pages: [], tags: [], authors: [], ...over }
}

describe('PagesListView', () => {
  it('counts untagged pages in the grand total (I-1 regression)', async () => {
    // Two pages tagged "a", one untagged. A tag-sum total would report 2;
    // the grand total must be 3.
    fetchPagesDataset.mockResolvedValue(
      dataset({
        pages: [
          page('p1', { tags: ['a'], primaryTagId: 'a' }),
          page('p2', { tags: ['a'], primaryTagId: 'a' }),
          page('p3'),
        ],
        tags: [tag('a', 2)],
      }),
    )

    render(<PagesListView onNavigate={() => {}} />)

    expect(await screen.findByText(/3 pages · organized by tags/)).toBeTruthy()
    expect(screen.getByText('3 of 3')).toBeTruthy()
  })

  it('renders the empty state when there are no pages', async () => {
    fetchPagesDataset.mockResolvedValue(dataset())
    render(<PagesListView onNavigate={() => {}} />)
    expect(await screen.findByText('No pages yet')).toBeTruthy()
  })

  it('shows an error with a retry that refetches', async () => {
    fetchPagesDataset.mockRejectedValueOnce(new Error('Boom'))
    render(<PagesListView onNavigate={() => {}} />)

    expect(await screen.findByText('Boom')).toBeTruthy()

    fetchPagesDataset.mockResolvedValueOnce(dataset({ pages: [page('p1')] }))
    fireEvent.click(screen.getByText('Try again'))

    await waitFor(() => expect(screen.getByText('1 of 1')).toBeTruthy())
  })

  it('navigates to the section editor when a row is opened', async () => {
    const onNavigate = vi.fn()
    fetchPagesDataset.mockResolvedValue(dataset({ pages: [page('about', { title: 'About' })] }))
    render(<PagesListView onNavigate={onNavigate} />)

    fireEvent.click(await screen.findByText('About'))
    expect(onNavigate).toHaveBeenCalledWith('/pages/about/edit')
  })
})

describe('PagesListView column visibility', () => {
  // Radix DropdownMenu opens on pointerdown (not click), which happy-dom
  // doesn't synthesize from fireEvent.click — open via keyboard instead.
  const openColumnsMenu = () =>
    fireEvent.keyDown(screen.getByRole('button', { name: 'Choose visible columns' }), {
      key: 'Enter',
    })
  // While the (modal) menu is open, Radix aria-hides the rest of the page,
  // so close it before asserting on table headers.
  const closeColumnsMenu = () => fireEvent.keyDown(screen.getByRole('menu'), { key: 'Escape' })

  it('hides and shows optional columns via the Columns menu, persisted', async () => {
    fetchPagesDataset.mockResolvedValue(
      dataset({ pages: [page('about', { title: 'About', tags: ['a'], primaryTagId: 'a' })] }),
    )
    render(<PagesListView onNavigate={() => {}} />)
    await screen.findByText('About')

    // All optional columns start visible.
    expect(screen.getByRole('columnheader', { name: /tags/i })).toBeTruthy()
    expect(screen.getByRole('columnheader', { name: /author/i })).toBeTruthy()
    expect(screen.getByRole('columnheader', { name: /seo/i })).toBeTruthy()

    openColumnsMenu()
    fireEvent.click(await screen.findByRole('menuitemcheckbox', { name: /tags/i }))
    closeColumnsMenu()

    expect(screen.queryByRole('columnheader', { name: /tags/i })).toBeNull()
    expect(screen.getByRole('columnheader', { name: /author/i })).toBeTruthy()
    expect(storage.get('actuate.pages.hiddenColumns')).toBe(JSON.stringify(['tags']))

    // Toggle back on.
    openColumnsMenu()
    fireEvent.click(await screen.findByRole('menuitemcheckbox', { name: /tags/i }))
    closeColumnsMenu()
    expect(screen.getByRole('columnheader', { name: /tags/i })).toBeTruthy()
    expect(storage.get('actuate.pages.hiddenColumns')).toBe(JSON.stringify([]))
  })

  it('restores hidden columns from localStorage on mount', async () => {
    storage.set('actuate.pages.hiddenColumns', JSON.stringify(['seo', 'author']))
    fetchPagesDataset.mockResolvedValue(dataset({ pages: [page('about', { title: 'About' })] }))
    render(<PagesListView onNavigate={() => {}} />)
    await screen.findByText('About')

    expect(screen.queryByRole('columnheader', { name: /seo/i })).toBeNull()
    expect(screen.queryByRole('columnheader', { name: /author/i })).toBeNull()
    expect(screen.getByRole('columnheader', { name: /tags/i })).toBeTruthy()
  })

  it('falls back to the title sort when the active sort column is hidden', async () => {
    fetchPagesDataset.mockResolvedValue(
      dataset({
        pages: [
          page('b', { title: 'Bravo', seoScore: 10 }),
          page('a', { title: 'Alpha', seoScore: 90 }),
        ],
      }),
    )
    render(<PagesListView onNavigate={() => {}} />)
    await screen.findByText('Alpha')

    // Sort by SEO (desc default) → Alpha (90) first either way, but the
    // header carries aria-sort.
    fireEvent.click(screen.getByRole('button', { name: /^SEO$/i }))
    expect(screen.getByRole('columnheader', { name: /seo/i }).getAttribute('aria-sort')).toBe(
      'descending',
    )

    openColumnsMenu()
    fireEvent.click(await screen.findByRole('menuitemcheckbox', { name: /seo score/i }))
    closeColumnsMenu()

    // SEO column gone; Title carries the (reset) ascending sort.
    expect(screen.queryByRole('columnheader', { name: /seo/i })).toBeNull()
    expect(screen.getByRole('columnheader', { name: /title/i }).getAttribute('aria-sort')).toBe(
      'ascending',
    )
  })
})

describe('PagesListView tree mode', () => {
  function hierarchyDataset() {
    return dataset({
      pages: [
        page('about', { title: 'About', path: '/about' }),
        page('team', { title: 'Team', path: '/about/team' }),
        page('blog', { title: 'Blog', path: '/blog' }),
      ],
    })
  }

  it('nests children and toggles expand/collapse', async () => {
    fetchPagesDataset.mockResolvedValue(hierarchyDataset())
    render(<PagesListView onNavigate={() => {}} />)
    await screen.findByText('About')

    fireEvent.click(screen.getByRole('button', { name: 'Tree view' }))

    // Root with children starts expanded → child visible; tree subtitle.
    expect(screen.getByText(/3 pages total/)).toBeTruthy()
    expect(screen.getByText('Team')).toBeTruthy()

    fireEvent.click(screen.getByRole('button', { name: 'Collapse About' }))
    expect(screen.queryByText('Team')).toBeNull()

    fireEvent.click(screen.getByRole('button', { name: 'Expand About' }))
    expect(screen.getByText('Team')).toBeTruthy()
  })

  it('flattens the tree while a status filter is active (expand state ignored)', async () => {
    fetchPagesDataset.mockResolvedValue(
      dataset({
        pages: [
          page('about', { title: 'About', path: '/about' }),
          page('team', { title: 'Team', path: '/about/team', status: 'DRAFT' }),
        ],
      }),
    )
    render(<PagesListView onNavigate={() => {}} />)
    await screen.findByText('About')

    fireEvent.click(screen.getByRole('button', { name: 'Tree view' }))
    fireEvent.click(screen.getByRole('button', { name: 'Collapse About' }))
    expect(screen.queryByText('Team')).toBeNull()

    // DRAFT filter → the collapsed child surfaces as a depth-0 match.
    fireEvent.change(screen.getByDisplayValue('All statuses'), { target: { value: 'DRAFT' } })
    expect(await screen.findByText('Team')).toBeTruthy()
    expect(screen.queryByText('About')).toBeNull()
    // Flattened rows carry no expand affordance.
    expect(screen.queryByRole('button', { name: /Expand|Collapse/ })).toBeNull()
  })

  it('routes "Add sub-page" to /pages/new with the parent preselected', async () => {
    const onNavigate = vi.fn()
    fetchPagesDataset.mockResolvedValue(hierarchyDataset())
    render(<PagesListView onNavigate={onNavigate} />)
    await screen.findByText('About')

    fireEvent.click(screen.getByRole('button', { name: 'Tree view' }))
    fireEvent.click(screen.getByRole('button', { name: 'Add sub-page under "Blog"' }))
    expect(onNavigate).toHaveBeenCalledWith('/pages/new?parent=blog')
  })

  it('remembers the tree preference via localStorage', async () => {
    fetchPagesDataset.mockResolvedValue(hierarchyDataset())
    const first = render(<PagesListView onNavigate={() => {}} />)
    await screen.findByText('About')
    fireEvent.click(screen.getByRole('button', { name: 'Tree view' }))
    expect(storage.get('actuate.pages.viewMode')).toBe('tree')
    first.unmount()

    fetchPagesDataset.mockResolvedValue(hierarchyDataset())
    render(<PagesListView onNavigate={() => {}} />)
    // Mounts straight into tree mode → tree subtitle.
    expect(await screen.findByText(/3 pages total/)).toBeTruthy()
  })
})
