import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import '@testing-library/jest-dom/vitest'
import React from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { HookInventoryItem, HookInventoryResponse } from '../../lib/collector-types'
import { fetchHookControls, fetchHookInventory, repairHookControls, setHookControl } from '../../lib/collector-client'
import { HooksContent } from './HooksContent'

vi.mock('../../lib/collector-client', async () => {
  const actual = await vi.importActual<typeof import('../../lib/collector-client')>(
    '../../lib/collector-client',
  )
  return {
    ...actual,
    fetchHookInventory: vi.fn(),
    fetchHookControls: vi.fn(),
    setHookControl: vi.fn(),
    repairHookControls: vi.fn(),
  }
})

function hook(overrides: Partial<HookInventoryItem> & { id: string }): HookInventoryItem {
  return {
    source: '/home/user/.claude/settings.json',
    scope: 'user',
    event: 'PreToolUse',
    matcher: 'Bash',
    command: '/home/user/.claude/hooks/gate.sh',
    commandPath: '/home/user/.claude/hooks/gate.sh',
    exists: true,
    executable: true,
    runtime: 'sh',
    runtimeResolves: true,
    status: 'OK',
    problem: null,
    lastExecutionAt: null,
    evidenceSource: null,
    ...overrides,
  }
}

function response(hooks: HookInventoryItem[], sourceErrors: HookInventoryResponse['sourceErrors'] = []): HookInventoryResponse {
  return {
    generatedAt: '2026-08-08T00:00:00.000Z',
    sources: ['/home/user/.claude/settings.json'],
    sourceErrors,
    hooks,
  }
}

function renderContent() {
  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
  return render(
    <QueryClientProvider client={queryClient}>
      <HooksContent />
    </QueryClientProvider>,
  )
}

const inventoryMock = vi.mocked(fetchHookInventory)
const controlsMock = vi.mocked(fetchHookControls)
const setControlMock = vi.mocked(setHookControl)
const repairMock = vi.mocked(repairHookControls)

describe('HooksContent', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    controlsMock.mockResolvedValue({ controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': true } }, issue: null })
  })

  afterEach(() => {
    cleanup()
  })

  it('places the fetched control before hook counts', async () => {
    inventoryMock.mockResolvedValue(response([]))
    renderContent()
    const toggle = await screen.findByLabelText('Block agent background jobs')
    expect(toggle).toBeChecked()
    expect(toggle.compareDocumentPosition(screen.getByText('0 hooks')) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
  })

  it('disables the control before the initial GET resolves and during mutation', async () => {
    inventoryMock.mockResolvedValue(response([]))
    controlsMock.mockImplementation(() => new Promise(() => undefined))
    renderContent()
    const toggle = await screen.findByLabelText('Block agent background jobs')
    expect(toggle).toBeDisabled()
  })

  it('retains confirmed state and surfaces mutation errors', async () => {
    inventoryMock.mockResolvedValue(response([]))
    setControlMock.mockRejectedValue(new Error('Write failed'))
    renderContent()
    const toggle = await screen.findByLabelText('Block agent background jobs')
    fireEvent.click(toggle)
    expect(await screen.findByRole('alert')).toHaveTextContent('Write failed')
    expect(toggle).toBeChecked()
  })


  it('adopts observed state and offers reconfirm when persistence is indeterminate', async () => {
    inventoryMock.mockResolvedValue(response([]))
    controlsMock.mockResolvedValue({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': false } },
      issue: null,
      persistence: 'indeterminate',
      persistenceDetail: 'parent directory fsync failed after commit',
    })
    setControlMock.mockResolvedValue({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': false } },
      issue: null,
    })
    renderContent()
    const toggle = await screen.findByLabelText('Block agent background jobs')
    expect(toggle).not.toBeChecked()
    expect(toggle).toBeDisabled()
    expect(screen.getByRole('alert')).toHaveTextContent('parent directory fsync failed after commit')
    fireEvent.click(screen.getByRole('button', { name: 'Reconfirm hook controls' }))
    await waitFor(() => expect(setControlMock).toHaveBeenCalledWith('background-jobs-blocker', false))
  })

  it('retains observed state and surfaces reconfirm errors', async () => {
    inventoryMock.mockResolvedValue(response([]))
    controlsMock.mockResolvedValue({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': false } },
      issue: null,
      persistence: 'indeterminate',
      persistenceDetail: 'parent directory fsync failed after commit',
    })
    setControlMock.mockRejectedValue(new Error('Reconfirm failed'))
    renderContent()
    const toggle = await screen.findByLabelText('Block agent background jobs')
    expect(toggle).not.toBeChecked()
    fireEvent.click(screen.getByRole('button', { name: 'Reconfirm hook controls' }))
    expect(await screen.findByRole('alert')).toHaveTextContent('Reconfirm failed')
    expect(toggle).not.toBeChecked()
  })

  it('disables the toggle and offers repair when the config issue is non-null', async () => {
    inventoryMock.mockResolvedValue(response([]))
    controlsMock.mockResolvedValue({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': true } },
      issue: { code: 'invalid-hook-controls', detail: 'hook-controls.json is not valid JSON', repairable: true },
    })
    repairMock.mockResolvedValue({ controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': true } }, issue: null })
    renderContent()
    const toggle = await screen.findByLabelText('Block agent background jobs')
    expect(toggle).toBeDisabled()
    fireEvent.click(screen.getByRole('button', { name: 'Repair hook controls' }))
    await waitFor(() => expect(repairMock).toHaveBeenCalled())
  })

  it('groups hooks by event and flags dead ones', async () => {
    inventoryMock.mockResolvedValue(response([
      hook({ id: 'a', event: 'PreToolUse' }),
      hook({
        id: 'b',
        event: 'Stop',
        matcher: null,
        command: '/home/user/.claude/hooks/gone.sh',
        commandPath: '/home/user/.claude/hooks/gone.sh',
        exists: false,
        executable: false,
        runtime: null,
        runtimeResolves: null,
        status: 'DEAD',
        problem: 'hook target is missing',
      }),
    ]))
    renderContent()

    const preToolUse = within(await screen.findByTestId('hooks-table-PreToolUse'))
    expect(preToolUse.getByText('OK')).toBeInTheDocument()

    const stop = within(screen.getByTestId('hooks-table-Stop'))
    expect(stop.getByText('DEAD')).toBeInTheDocument()
    expect(stop.getByText('hook target is missing')).toBeInTheDocument()

    expect(screen.getByTestId('hooks-dead-count')).toHaveTextContent('1 dead')
  })

  it('shows unresolved runtimes and the unobserved count', async () => {
    inventoryMock.mockResolvedValue(response([
      hook({
        id: 'a',
        runtime: 'python9',
        runtimeResolves: false,
        status: 'DEAD',
        problem: 'runtime python9 does not resolve to an executable',
      }),
      hook({
        id: 'b',
        event: 'SessionStart',
        matcher: null,
        runtime: null,
        runtimeResolves: null,
        status: 'UNOBSERVED',
        problem: 'hook target is unreadable, so its runtime cannot be verified',
      }),
    ]))
    renderContent()

    expect(await screen.findByText('python9')).toBeInTheDocument()
    expect(screen.getByText('UNOBSERVED')).toBeInTheDocument()
    expect(screen.getByTestId('hooks-unobserved-count')).toHaveTextContent('1 unobserved')
  })

  it('filters hooks across events', async () => {
    inventoryMock.mockResolvedValue(response([
      hook({ id: 'a', event: 'PreToolUse', command: '/home/user/.claude/hooks/gate.sh' }),
      hook({ id: 'b', event: 'Stop', matcher: null, command: '/home/user/.claude/hooks/notify.sh' }),
    ]))
    renderContent()

    await screen.findByTestId('hooks-table-PreToolUse')
    fireEvent.change(screen.getByLabelText('Filter hooks'), { target: { value: 'notify' } })

    expect(screen.queryByTestId('hooks-table-PreToolUse')).not.toBeInTheDocument()
    expect(screen.getByTestId('hooks-table-Stop')).toBeInTheDocument()
  })


  it('renders unchecked before hook controls load', async () => {
    inventoryMock.mockResolvedValue(response([]))
    controlsMock.mockImplementation(() => new Promise(() => undefined))
    renderContent()
    const toggle = await screen.findByLabelText('Block agent background jobs')
    expect(toggle).not.toBeChecked()
    expect(toggle).toBeDisabled()
  })



  it('does not offer repair for nonrepairable issues but still surfaces issue detail', async () => {
    inventoryMock.mockResolvedValue(response([]))
    controlsMock.mockResolvedValue({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': true } },
      issue: { code: 'invalid-hook-controls', detail: 'hook-controls.json is not a regular file', repairable: false },
    })
    renderContent()
    const toggle = await screen.findByLabelText('Block agent background jobs')
    expect(toggle).toBeDisabled()
    expect(screen.getByRole('alert')).toHaveTextContent('hook-controls.json is not a regular file')
    expect(screen.queryByRole('button', { name: 'Repair hook controls' })).not.toBeInTheDocument()
  })

  it('surfaces both issue and persistence detail when both are present', async () => {
    inventoryMock.mockResolvedValue(response([]))
    controlsMock.mockResolvedValue({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': false } },
      issue: { code: 'invalid-hook-controls', detail: 'hook-controls.json is not valid JSON', repairable: true },
      persistence: 'indeterminate',
      persistenceDetail: 'parent directory fsync failed after commit',
    })
    renderContent()
    expect(await screen.findByRole('alert')).toHaveTextContent('hook-controls.json is not valid JSON; parent directory fsync failed after commit')
    expect(screen.queryByRole('button', { name: 'Reconfirm hook controls' })).not.toBeInTheDocument()
  })
  it('does not show reconfirm when config issue is non-null', async () => {
    inventoryMock.mockResolvedValue(response([]))
    controlsMock.mockResolvedValue({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': false } },
      issue: { code: 'invalid-hook-controls', detail: 'hook-controls.json is not valid JSON', repairable: true },
      persistence: 'indeterminate',
      persistenceDetail: 'parent directory fsync failed after commit',
    })
    renderContent()
    await screen.findByLabelText('Block agent background jobs')
    expect(screen.queryByRole('button', { name: 'Reconfirm hook controls' })).not.toBeInTheDocument()
    expect(screen.getByRole('button', { name: 'Repair hook controls' })).toBeInTheDocument()
  })

  it('mutually excludes repair and reconfirm pending states', async () => {
    inventoryMock.mockResolvedValue(response([]))
    controlsMock.mockResolvedValue({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': false } },
      issue: null,
      persistence: 'indeterminate',
      persistenceDetail: 'parent directory fsync failed after commit',
    })
    setControlMock.mockImplementation(() => new Promise(() => undefined))
    renderContent()
    const reconfirm = await screen.findByRole('button', { name: 'Reconfirm hook controls' })
    fireEvent.click(reconfirm)
    await waitFor(() => expect(screen.getByRole('button', { name: 'Reconfirm hook controls' })).toBeDisabled())

    controlsMock.mockResolvedValue({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': true } },
      issue: { code: 'invalid-hook-controls', detail: 'hook-controls.json is not valid JSON', repairable: true },
    })
    repairMock.mockImplementation(() => new Promise(() => undefined))
    cleanup()
    renderContent()
    const repair = await screen.findByRole('button', { name: 'Repair hook controls' })
    fireEvent.click(repair)
    await waitFor(() => expect(screen.getByRole('button', { name: 'Repair hook controls' })).toBeDisabled())
  })

  it('clears stale mutation error after successful toggle and confirmed GET', async () => {
    inventoryMock.mockResolvedValue(response([]))
    setControlMock.mockRejectedValueOnce(new Error('Write failed'))
    controlsMock.mockResolvedValueOnce({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': true } },
      issue: null,
    })
    renderContent()
    const toggle = await screen.findByLabelText('Block agent background jobs')
    fireEvent.click(toggle)
    expect(await screen.findByRole('alert')).toHaveTextContent('Write failed')
    setControlMock.mockResolvedValueOnce({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': false } },
      issue: null,
    })
    controlsMock.mockResolvedValueOnce({
      controls: { version: 'hook-controls/v1', hooks: { 'background-jobs-blocker': false } },
      issue: null,
    })
    fireEvent.click(toggle)
    await waitFor(() => expect(screen.queryByRole('alert')).not.toBeInTheDocument())
  })

  it('renders the empty state and source errors', async () => {
    inventoryMock.mockResolvedValue(response([], [{ source: '/home/user/.claude/tools.json', error: 'Unexpected token' }]))
    renderContent()

    expect(await screen.findByTestId('hooks-empty')).toHaveTextContent('No hooks configured')
    expect(screen.getByTestId('hooks-source-errors')).toHaveTextContent('/home/user/.claude/tools.json')
  })
})
