import type { ReactNode } from 'react'
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { afterEach, beforeEach, expect, test, vi } from 'vitest'
import App from './App'

const dragAndDrop = vi.hoisted(() => ({
  onTaskDrop: null as ((taskId: string, status: string) => void) | null,
}))

vi.mock('./TaskDragAndDrop', () => ({
  TaskDragDropProvider: ({
    children,
    onTaskDrop,
  }: {
    children: ReactNode
    onTaskDrop: (taskId: string, status: string) => void
  }) => {
    dragAndDrop.onTaskDrop = onTaskDrop
    return children
  },
  useTaskDraggable: () => ({
    ref: () => {},
    isDragging: false,
  }),
  useTaskColumnDropTarget: () => ({
    ref: () => {},
    isDropTarget: false,
  }),
}))

const users = [
  {
    id: 'd56b54dd-31b0-4d71-8a10-82464be59a61',
    name: 'Urban',
    createdAt: '2026-07-24T10:15:30Z',
  },
  {
    id: '2213a673-c859-462b-a031-7b68b6f01c73',
    name: 'Anna',
    createdAt: '2026-07-24T10:16:30Z',
  },
]

const tasks = [
  {
    id: '00000000-0000-0000-0000-000000000001',
    title: 'Dammsuga',
    description: 'Bottenvåningen',
    status: 'WAITING',
    points: 7,
    assignee: null,
    createdAt: '2026-07-24T10:00:00Z',
  },
  {
    id: '00000000-0000-0000-0000-000000000002',
    title: 'Diska',
    description: null,
    status: 'IN_PROGRESS',
    points: 3,
    assignee: { id: users[1].id, name: users[1].name },
    createdAt: '2026-07-24T10:01:00Z',
  },
  {
    id: '00000000-0000-0000-0000-000000000003',
    title: 'Vattna blommor',
    description: null,
    status: 'COMPLETED',
    points: 5,
    assignee: null,
    createdAt: '2026-07-24T10:02:00Z',
  },
]

beforeEach(() => {
  window.localStorage.clear()
  dragAndDrop.onTaskDrop = null
})

afterEach(() => {
  cleanup()
  vi.restoreAllMocks()
})

test('tom användarlista visar Skapa användare', async () => {
  mockJsonResponse([])

  render(<App />)

  expect(await screen.findByRole('heading', { name: 'Skapa användare' })).toBeInTheDocument()
})

test('befintliga användare utan lokalt val visar Vem är du', async () => {
  mockJsonResponse(users)

  render(<App />)

  expect(await screen.findByRole('heading', { name: 'Vem är du?' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Urban' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Anna' })).toBeInTheDocument()
})

test('val av användare lagrar id och visar brädan', async () => {
  mockUsersAndTasks(users, [])
  render(<App />)

  fireEvent.click(await screen.findByRole('button', { name: 'Urban' }))

  expect(await screen.findByRole('heading', { name: 'Uppgifter' })).toBeInTheDocument()
  expect(screen.getByText('Urban')).toBeInTheDocument()
  expect(window.localStorage.getItem('hemhub.activeUserId')).toBe(users[0].id)
})

test('skapad användare blir automatiskt aktiv', async () => {
  const createdUser = users[0]
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse([]))
  fetchMock.mockResolvedValueOnce(jsonResponse(createdUser, 201))
  fetchMock.mockResolvedValueOnce(jsonResponse([]))

  render(<App />)

  fireEvent.change(await screen.findByLabelText('Namn'), { target: { value: '  Urban  ' } })
  fireEvent.click(screen.getByRole('button', { name: 'Skapa användare' }))

  expect(await screen.findByRole('heading', { name: 'Uppgifter' })).toBeInTheDocument()
  expect(window.localStorage.getItem('hemhub.activeUserId')).toBe(createdUser.id)
  expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Urban' }),
  })
})

test('skapandefel för användare visas utan att namnet försvinner', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse([]))
  fetchMock.mockResolvedValueOnce(
    jsonResponse(
      {
        code: 'USER_NAME_ALREADY_EXISTS',
        message: 'En användare med det namnet finns redan.',
      },
      409,
    ),
  )

  render(<App />)

  const input = await screen.findByLabelText('Namn')
  fireEvent.change(input, { target: { value: 'Urban' } })
  fireEvent.click(screen.getByRole('button', { name: 'Skapa användare' }))

  expect(await screen.findByRole('alert')).toHaveTextContent(
    'En användare med det namnet finns redan.',
  )
  expect(input).toHaveValue('Urban')
})

test('Logga ut rensar valt id och väljer inte användaren automatiskt', async () => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  mockUsersAndTasks(users, [])
  render(<App />)

  fireEvent.click(await screen.findByRole('button', { name: 'Logga ut' }))

  expect(screen.getByRole('heading', { name: 'Vem är du?' })).toBeInTheDocument()
  expect(screen.getByRole('button', { name: 'Urban' })).toBeInTheDocument()
  expect(screen.queryByRole('heading', { name: 'Uppgifter' })).not.toBeInTheDocument()
  expect(window.localStorage.getItem('hemhub.activeUserId')).toBeNull()
})

test('hämtningsfel för användare visas som fel och kan återförsökas', async () => {
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockRejectedValueOnce(new Error('Nätverksfel'))
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  render(<App />)

  expect(await screen.findByText('Kunde inte ansluta till HemHub.')).toBeInTheDocument()
  expect(screen.queryByRole('heading', { name: 'Skapa användare' })).not.toBeInTheDocument()

  fireEvent.click(screen.getByRole('button', { name: 'Försök igen' }))

  expect(await screen.findByRole('heading', { name: 'Vem är du?' })).toBeInTheDocument()
  expect(fetchMock).toHaveBeenCalledTimes(2)
})

test('ogiltigt lagrat användar-id rensas', async () => {
  window.localStorage.setItem('hemhub.activeUserId', 'finns-inte')
  mockJsonResponse(users)

  render(<App />)

  expect(await screen.findByRole('heading', { name: 'Vem är du?' })).toBeInTheDocument()
  expect(window.localStorage.getItem('hemhub.activeUserId')).toBeNull()
})

test('brädan visar tre kolumner och grupperar hämtade uppgifter', async () => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  mockUsersAndTasks(users, tasks)

  render(<App />)

  await screen.findByText('Dammsuga')
  const waiting = await screen.findByRole('region', { name: 'Väntande' })
  const inProgress = screen.getByRole('region', { name: 'Pågående' })
  const completed = screen.getByRole('region', { name: 'Klart' })

  expect(within(waiting).getByText('Dammsuga')).toBeInTheDocument()
  expect(within(waiting).getByText('Bottenvåningen')).toBeInTheDocument()
  expect(within(waiting).getByText('7 p')).toBeInTheDocument()
  expect(within(inProgress).getByText('Diska')).toBeInTheDocument()
  expect(within(completed).getByText('Vattna blommor')).toBeInTheDocument()
  expect(screen.queryByText(/Inga uppgifter/i)).not.toBeInTheDocument()
})

test('Ny uppgift öppnar modalen med fokus i titelfältet', async () => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  mockUsersAndTasks(users, [])
  render(<App />)

  fireEvent.click(await screen.findByRole('button', { name: 'Ny uppgift' }))

  expect(screen.getByRole('dialog', { name: 'Skapa ny uppgift' })).toBeInTheDocument()
  expect(screen.getByLabelText('Titel')).toHaveFocus()
  expect(screen.getByLabelText('Poäng')).toHaveValue(1)
  expect(screen.getByLabelText('Tilldela')).toHaveValue('')
  expect(within(screen.getByLabelText('Tilldela')).getByRole('option', { name: 'Ingen' }))
    .toBeInTheDocument()
  expect(within(screen.getByLabelText('Tilldela')).getByRole('option', { name: 'Urban' }))
    .toBeInTheDocument()
  expect(within(screen.getByLabelText('Tilldela')).getByRole('option', { name: 'Anna' }))
    .toBeInTheDocument()
})

test.each([
  {
    sätt: 'stängningskrysset',
    close: () => fireEvent.click(screen.getByRole('button', { name: 'Stäng' })),
  },
  {
    sätt: 'Escape',
    close: () => fireEvent.keyDown(window, { key: 'Escape' }),
  },
  {
    sätt: 'modalens bakgrund',
    close: () => {
      const backdrop = screen.getByRole('dialog', { name: 'Skapa ny uppgift' }).parentElement

      if (!backdrop) {
        throw new Error('Modalens bakgrund saknas')
      }

      fireEvent.mouseDown(backdrop)
    },
  },
])('modalen stängs med $sätt och rensar formuläret', async ({ close }) => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  mockUsersAndTasks(users, [])
  render(<App />)

  fireEvent.click(await screen.findByRole('button', { name: 'Ny uppgift' }))
  fireEvent.change(screen.getByLabelText('Titel'), { target: { value: 'Dammsuga' } })
  fireEvent.change(screen.getByLabelText('Beskrivning (valfri)'), {
    target: { value: 'Bottenvåningen' },
  })
  fireEvent.change(screen.getByLabelText('Poäng'), { target: { value: '7' } })

  close()

  expect(screen.queryByRole('dialog', { name: 'Skapa ny uppgift' })).not.toBeInTheDocument()

  fireEvent.click(screen.getByRole('button', { name: 'Ny uppgift' }))

  expect(screen.getByLabelText('Titel')).toHaveValue('')
  expect(screen.getByLabelText('Beskrivning (valfri)')).toHaveValue('')
  expect(screen.getByLabelText('Poäng')).toHaveValue(1)
})

test('en skapad uppgift visas längst ned i Väntande och modalen stängs', async () => {
  const createdTask = {
    id: '00000000-0000-0000-0000-000000000004',
    title: 'Putsa fönster',
    description: 'Köket',
    status: 'WAITING',
    points: 7,
    assignee: { id: users[1].id, name: users[1].name },
    createdAt: '2026-07-24T10:03:00Z',
  }
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([tasks[0]]))
  fetchMock.mockResolvedValueOnce(jsonResponse(createdTask, 201))

  render(<App />)

  await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2))
  fireEvent.click(screen.getByRole('button', { name: 'Ny uppgift' }))
  fireEvent.change(screen.getByLabelText('Titel'), { target: { value: '  Putsa fönster  ' } })
  fireEvent.change(screen.getByLabelText('Beskrivning (valfri)'), {
    target: { value: '  Köket  ' },
  })
  fireEvent.change(screen.getByLabelText('Poäng'), { target: { value: '7' } })
  fireEvent.change(screen.getByLabelText('Tilldela'), { target: { value: users[1].id } })
  fireEvent.click(screen.getByRole('button', { name: 'Skapa uppgift' }))

  await waitFor(() =>
    expect(screen.queryByRole('dialog', { name: 'Skapa ny uppgift' })).not.toBeInTheDocument(),
  )
  const waiting = await screen.findByRole('region', { name: 'Väntande' })
  expect(
    within(waiting)
      .getAllByRole('article')
      .map((card) => within(card).getByRole('heading').textContent),
  ).toEqual(['Dammsuga', 'Putsa fönster'])
  expect(fetchMock).toHaveBeenLastCalledWith('/api/tasks', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      title: 'Putsa fönster',
      description: 'Köket',
      points: 7,
      assigneeId: users[1].id,
    }),
  })

  fireEvent.click(screen.getByRole('button', { name: 'Ny uppgift' }))
  expect(screen.getByLabelText('Poäng')).toHaveValue(1)
  expect(screen.getByLabelText('Tilldela')).toHaveValue('')
})

test('Ingen skickas som null när en uppgift skapas', async () => {
  const createdTask = {
    ...tasks[0],
    id: '00000000-0000-0000-0000-000000000010',
    title: 'Torka bordet',
  }
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([]))
  fetchMock.mockResolvedValueOnce(jsonResponse(createdTask, 201))
  render(<App />)

  await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2))
  fireEvent.click(screen.getByRole('button', { name: 'Ny uppgift' }))
  fireEvent.change(screen.getByLabelText('Titel'), { target: { value: 'Torka bordet' } })
  fireEvent.click(screen.getByRole('button', { name: 'Skapa uppgift' }))

  await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(3))
  expect(fetchMock).toHaveBeenLastCalledWith('/api/tasks', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      title: 'Torka bordet',
      description: null,
      points: 1,
      assigneeId: null,
    }),
  })
})

test('statusanrop byter inte en befintlig ansvarig', async () => {
  const assignedWaiting = {
    ...tasks[0],
    assignee: { id: users[1].id, name: users[1].name },
  }
  const updatedTask = { ...assignedWaiting, status: 'IN_PROGRESS' }
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([assignedWaiting]))
  fetchMock.mockResolvedValueOnce(jsonResponse(updatedTask))
  render(<App />)

  const card = (await screen.findByText('Dammsuga')).closest('article')!
  fireEvent.click(within(card).getByRole('button', { name: 'Påbörja' }))

  const inProgress = screen.getByRole('region', { name: 'Pågående' })
  expect(await within(inProgress).findByText('Anna')).toBeInTheDocument()
  expect(within(inProgress).queryByText('Urban')).not.toBeInTheDocument()
})

test('alla statusar har redigerbar tilldelning med statusberoende alternativ', async () => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  mockUsersAndTasks(users, tasks)
  render(<App />)

  const waitingCard = (await screen.findByText('Dammsuga')).closest('article')
  const inProgressCard = screen.getByText('Diska').closest('article')
  const completedCard = screen.getByText('Vattna blommor').closest('article')

  expect(waitingCard).not.toBeNull()
  expect(inProgressCard).not.toBeNull()
  expect(completedCard).not.toBeNull()
  expect(within(waitingCard!).getByRole('button', { name: 'Ändra ansvarig för Dammsuga' }))
    .toHaveTextContent('Ta uppgift')
  expect(within(inProgressCard!).getByText('Anna')).toBeInTheDocument()
  expect(within(completedCard!).getByText('Otilldelad')).toBeInTheDocument()

  fireEvent.click(
    within(inProgressCard!).getByRole('button', { name: 'Ändra ansvarig för Diska' }),
  )
  expect(
    within(inProgressCard!).queryByRole('option', { name: 'Ingen' }),
  ).not.toBeInTheDocument()

  fireEvent.click(
    within(completedCard!).getByRole('button', {
      name: 'Ändra ansvarig för Vattna blommor',
    }),
  )
  expect(within(completedCard!).getByRole('option', { name: 'Ingen' })).toBeInTheDocument()
})

test('visar rätt statusknappar för varje kolumn', async () => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  mockUsersAndTasks(users, tasks)
  render(<App />)

  const waitingCard = (await screen.findByText('Dammsuga')).closest('article')!
  const inProgressCard = screen.getByText('Diska').closest('article')!
  const completedCard = screen.getByText('Vattna blommor').closest('article')!

  expect(within(waitingCard).getByRole('button', { name: 'Påbörja' })).toBeInTheDocument()
  expect(within(waitingCard).getByRole('button', { name: 'Markera klar' })).toBeInTheDocument()
  expect(within(inProgressCard).getByRole('button', { name: 'Till Väntande' }))
    .toBeInTheDocument()
  expect(within(inProgressCard).getByRole('button', { name: 'Markera klar' }))
    .toBeInTheDocument()
  expect(within(completedCard).getByRole('button', { name: 'Till Väntande' }))
    .toBeInTheDocument()
  expect(within(completedCard).getByRole('button', { name: 'Påbörja igen' }))
    .toBeInTheDocument()
})

test.each([
  { task: tasks[0], button: 'Påbörja', target: 'IN_PROGRESS' },
  { task: tasks[0], button: 'Markera klar', target: 'COMPLETED' },
  { task: tasks[1], button: 'Till Väntande', target: 'WAITING' },
  { task: tasks[1], button: 'Markera klar', target: 'COMPLETED' },
  { task: tasks[2], button: 'Till Väntande', target: 'WAITING' },
  { task: tasks[2], button: 'Påbörja igen', target: 'IN_PROGRESS' },
])('$button skickar status $target', async ({ task, button, target }) => {
  const updatedTask = {
    ...task,
    status: target,
    assignee:
      target === 'IN_PROGRESS' && !task.assignee
        ? { id: users[0].id, name: users[0].name }
        : task.assignee,
  }
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([task]))
  fetchMock.mockResolvedValueOnce(jsonResponse(updatedTask))
  render(<App />)

  const card = (await screen.findByText(task.title)).closest('article')!
  fireEvent.click(within(card).getByRole('button', { name: button }))

  await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(3))
  expect(fetchMock).toHaveBeenLastCalledWith(`/api/tasks/${task.id}/status`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      status: target,
      ...(target === 'IN_PROGRESS' ? { activeUserId: users[0].id } : {}),
    }),
  })
})

test('status uppdateras först efter serversvar och låser endast berört kort', async () => {
  const otherTask = {
    ...tasks[0],
    id: '00000000-0000-0000-0000-000000000010',
    title: 'Putsa fönster',
  }
  const updatedTask = {
    ...tasks[0],
    status: 'IN_PROGRESS',
    assignee: { id: users[0].id, name: users[0].name },
  }
  let resolveStatus!: (response: Response) => void
  const statusResponse = new Promise<Response>((resolve) => {
    resolveStatus = resolve
  })
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([tasks[0], otherTask]))
  fetchMock.mockReturnValueOnce(statusResponse)
  render(<App />)

  const waiting = await screen.findByRole('region', { name: 'Väntande' })
  const card = (await within(waiting).findByText('Dammsuga')).closest('article')!
  const otherCard = within(waiting).getByText('Putsa fönster').closest('article')!
  const startButton = within(card).getByRole('button', { name: 'Påbörja' })
  fireEvent.click(startButton)
  fireEvent.click(startButton)

  expect(within(waiting).getByText('Dammsuga')).toBeInTheDocument()
  expect(startButton).toBeDisabled()
  expect(within(card).getByRole('button', { name: 'Ändra ansvarig för Dammsuga' }))
    .toBeDisabled()
  expect(within(otherCard).getByRole('button', { name: 'Påbörja' })).toBeEnabled()
  expect(fetchMock).toHaveBeenCalledTimes(3)

  resolveStatus(jsonResponse(updatedTask))
  const inProgress = screen.getByRole('region', { name: 'Pågående' })
  expect(await within(inProgress).findByText('Dammsuga')).toBeInTheDocument()
  expect(within(inProgress).getByText('Urban')).toBeInTheDocument()
})

test('statusfel behåller tidigare status och ansvarig och visas på kortet', async () => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([tasks[0]]))
  fetchMock.mockResolvedValueOnce(
    jsonResponse(
      {
        code: 'TASK_REQUIRES_ASSIGNEE',
        message: 'En pågående uppgift måste ha en ansvarig.',
      },
      409,
    ),
  )
  render(<App />)

  const waiting = await screen.findByRole('region', { name: 'Väntande' })
  const card = (await within(waiting).findByText('Dammsuga')).closest('article')!
  fireEvent.click(within(card).getByRole('button', { name: 'Påbörja' }))

  expect(await within(card).findByRole('alert')).toHaveTextContent(
    'En pågående uppgift måste ha en ansvarig.',
  )
  expect(within(waiting).getByText('Dammsuga')).toBeInTheDocument()
  expect(within(card).getByText('Ta uppgift')).toBeInTheDocument()
})

test('drag flyttar optimistiskt, låser kortet och använder hela serverresponsen', async () => {
  const otherTask = {
    ...tasks[0],
    id: '00000000-0000-0000-0000-000000000010',
    title: 'Putsa fönster',
  }
  const serverTask = {
    ...tasks[0],
    status: 'COMPLETED',
    assignee: { id: users[1].id, name: users[1].name },
  }
  let resolveStatus!: (response: Response) => void
  const statusResponse = new Promise<Response>((resolve) => {
    resolveStatus = resolve
  })
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([tasks[0], otherTask]))
  fetchMock.mockReturnValueOnce(statusResponse)
  render(<App />)

  await screen.findByText('Dammsuga')
  act(() => dropTask(tasks[0].id, 'IN_PROGRESS'))

  const inProgress = screen.getByRole('region', { name: 'Pågående' })
  const optimisticCard = (await within(inProgress).findByText('Dammsuga')).closest('article')!
  const waiting = screen.getByRole('region', { name: 'Väntande' })
  const otherCard = within(waiting).getByText('Putsa fönster').closest('article')!

  expect(within(optimisticCard).getByText('Urban')).toBeInTheDocument()
  expect(optimisticCard).toHaveAttribute('aria-busy', 'true')
  expect(optimisticCard).toHaveClass('task-card-pending')
  expect(within(optimisticCard).getByRole('button', { name: 'Markera klar' })).toBeDisabled()
  expect(
    within(optimisticCard).getByRole('button', { name: 'Ändra ansvarig för Dammsuga' }),
  ).toBeDisabled()
  expect(within(otherCard).getByRole('button', { name: 'Påbörja' })).toBeEnabled()
  expect(fetchMock).toHaveBeenLastCalledWith(`/api/tasks/${tasks[0].id}/status`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      status: 'IN_PROGRESS',
      activeUserId: users[0].id,
    }),
  })

  act(() => dropTask(tasks[0].id, 'COMPLETED'))
  expect(fetchMock).toHaveBeenCalledTimes(3)

  await act(async () => resolveStatus(jsonResponse(serverTask)))

  const completed = screen.getByRole('region', { name: 'Klart' })
  const confirmedCard = (await within(completed).findByText('Dammsuga')).closest('article')!
  expect(within(confirmedCard).getByText('Anna')).toBeInTheDocument()
  expect(confirmedCard).not.toHaveAttribute('aria-busy')
})

test('dragfel återställer hela uppgiften och visar lokalt fel', async () => {
  let resolveStatus!: (response: Response) => void
  const statusResponse = new Promise<Response>((resolve) => {
    resolveStatus = resolve
  })
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([tasks[0]]))
  fetchMock.mockReturnValueOnce(statusResponse)
  render(<App />)

  await screen.findByText('Dammsuga')
  act(() => dropTask(tasks[0].id, 'IN_PROGRESS'))

  const inProgress = screen.getByRole('region', { name: 'Pågående' })
  expect(await within(inProgress).findByText('Urban')).toBeInTheDocument()

  await act(async () =>
    resolveStatus(
      jsonResponse(
        {
          code: 'TASK_REQUIRES_ASSIGNEE',
          message: 'En pågående uppgift måste ha en ansvarig.',
        },
        409,
      ),
    ),
  )

  const waiting = screen.getByRole('region', { name: 'Väntande' })
  const restoredCard = (await within(waiting).findByText('Dammsuga')).closest('article')!
  expect(within(restoredCard).getByText('Ta uppgift')).toBeInTheDocument()
  expect(within(restoredCard).queryByText('Urban')).not.toBeInTheDocument()
  expect(await within(restoredCard).findByRole('alert')).toHaveTextContent(
    'En pågående uppgift måste ha en ansvarig.',
  )
  expect(restoredCard).not.toHaveAttribute('aria-busy')
})

test('drag till Pågående behåller en befintlig ansvarig optimistiskt', async () => {
  const assignedTask = {
    ...tasks[0],
    assignee: { id: users[1].id, name: users[1].name },
  }
  let resolveStatus!: (response: Response) => void
  const statusResponse = new Promise<Response>((resolve) => {
    resolveStatus = resolve
  })
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([assignedTask]))
  fetchMock.mockReturnValueOnce(statusResponse)
  render(<App />)

  await screen.findByText('Dammsuga')
  act(() => dropTask(assignedTask.id, 'IN_PROGRESS'))

  const inProgress = screen.getByRole('region', { name: 'Pågående' })
  expect(await within(inProgress).findByText('Anna')).toBeInTheDocument()
  expect(within(inProgress).queryByText('Urban')).not.toBeInTheDocument()

  await act(async () =>
    resolveStatus(jsonResponse({ ...assignedTask, status: 'IN_PROGRESS' })),
  )
})

test('drop i samma kolumn är no-op', async () => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = mockUsersAndTasks(users, [tasks[0]])
  render(<App />)

  await screen.findByText('Dammsuga')
  act(() => dropTask(tasks[0].id, 'WAITING'))

  expect(fetchMock).toHaveBeenCalledTimes(2)
  expect(screen.getByText('Dammsuga').closest('article')).not.toHaveAttribute('aria-busy')
})

test('olika kort kan ha samtidiga optimistiska statusanrop', async () => {
  const otherTask = {
    ...tasks[0],
    id: '00000000-0000-0000-0000-000000000010',
    title: 'Putsa fönster',
  }
  const firstResponse = new Promise<Response>(() => {})
  const secondResponse = new Promise<Response>(() => {})
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([tasks[0], otherTask]))
  fetchMock.mockReturnValueOnce(firstResponse)
  fetchMock.mockReturnValueOnce(secondResponse)
  render(<App />)

  await screen.findByText('Dammsuga')
  act(() => {
    dropTask(tasks[0].id, 'IN_PROGRESS')
    dropTask(otherTask.id, 'COMPLETED')
  })

  expect(screen.getByText('Dammsuga').closest('article')).toHaveAttribute('aria-busy', 'true')
  expect(screen.getByText('Putsa fönster').closest('article')).toHaveAttribute(
    'aria-busy',
    'true',
  )
  expect(fetchMock).toHaveBeenCalledTimes(4)
})

test('ansvarig kan bytas i Pågående och tas bort i Klart', async () => {
  const changedInProgress = { ...tasks[1], assignee: { id: users[0].id, name: users[0].name } }
  const unassignedCompleted = { ...tasks[2], assignee: null }
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([tasks[1], { ...tasks[2], assignee: tasks[1].assignee }]))
  fetchMock.mockResolvedValueOnce(jsonResponse(changedInProgress))
  fetchMock.mockResolvedValueOnce(jsonResponse(unassignedCompleted))
  render(<App />)

  const inProgressCard = (await screen.findByText('Diska')).closest('article')!
  fireEvent.click(
    within(inProgressCard).getByRole('button', { name: 'Ändra ansvarig för Diska' }),
  )
  fireEvent.change(within(inProgressCard).getByRole('combobox'), {
    target: { value: users[0].id },
  })
  await waitFor(() =>
    expect(within(inProgressCard).getByRole('button', { name: 'Ändra ansvarig för Diska' }))
      .toHaveTextContent('Urban'),
  )

  const completedCard = screen.getByText('Vattna blommor').closest('article')!
  fireEvent.click(
    within(completedCard).getByRole('button', {
      name: 'Ändra ansvarig för Vattna blommor',
    }),
  )
  fireEvent.change(within(completedCard).getByRole('combobox'), { target: { value: '' } })

  await waitFor(() =>
    expect(fetchMock).toHaveBeenLastCalledWith(`/api/tasks/${tasks[2].id}/assignee`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ assigneeId: null }),
    }),
  )
})

test('val av ansvarig anropar endpointen och uppdaterar kortet efter svar', async () => {
  const updatedTask = { ...tasks[0], assignee: { id: users[1].id, name: users[1].name } }
  let resolveAssignment!: (response: Response) => void
  const assignmentResponse = new Promise<Response>((resolve) => {
    resolveAssignment = resolve
  })
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([tasks[0]]))
  fetchMock.mockReturnValueOnce(assignmentResponse)
  render(<App />)

  fireEvent.click(await screen.findByRole('button', { name: 'Ändra ansvarig för Dammsuga' }))
  const select = screen.getByRole('combobox', { name: 'Ansvarig för Dammsuga' })
  fireEvent.change(select, { target: { value: users[1].id } })

  expect(select).toBeDisabled()
  expect(within(select.closest('article')!).getByRole('button', { name: 'Påbörja' }))
    .toBeDisabled()
  expect(fetchMock).toHaveBeenLastCalledWith(`/api/tasks/${tasks[0].id}/assignee`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ assigneeId: users[1].id }),
  })

  resolveAssignment(jsonResponse(updatedTask))
  await waitFor(() =>
    expect(screen.getByRole('button', { name: 'Ändra ansvarig för Dammsuga' }))
      .toHaveTextContent('Anna'),
  )
  expect(screen.queryByRole('combobox', { name: 'Ansvarig för Dammsuga' })).not.toBeInTheDocument()
})

test('val av Ingen av-tilldelar en väntande uppgift', async () => {
  const assignedTask = { ...tasks[0], assignee: { id: users[1].id, name: users[1].name } }
  const unassignedTask = { ...assignedTask, assignee: null }
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([assignedTask]))
  fetchMock.mockResolvedValueOnce(jsonResponse(unassignedTask))
  render(<App />)

  fireEvent.click(await screen.findByRole('button', { name: 'Ändra ansvarig för Dammsuga' }))
  fireEvent.change(screen.getByRole('combobox', { name: 'Ansvarig för Dammsuga' }), {
    target: { value: '' },
  })

  await screen.findByText('Ta uppgift')
  expect(fetchMock).toHaveBeenLastCalledWith(`/api/tasks/${tasks[0].id}/assignee`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ assigneeId: null }),
  })
})

test('misslyckad tilldelning behåller ansvarig och visar fel', async () => {
  const assignedTask = { ...tasks[0], assignee: { id: users[1].id, name: users[1].name } }
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([assignedTask]))
  fetchMock.mockResolvedValueOnce(
    jsonResponse({ code: 'USER_NOT_FOUND', message: 'Användaren finns inte.' }, 404),
  )
  render(<App />)

  fireEvent.click(await screen.findByRole('button', { name: 'Ändra ansvarig för Dammsuga' }))
  fireEvent.change(screen.getByRole('combobox', { name: 'Ansvarig för Dammsuga' }), {
    target: { value: users[0].id },
  })

  expect(await screen.findByRole('alert')).toHaveTextContent('Användaren finns inte.')
  expect(screen.getByRole('combobox', { name: 'Ansvarig för Dammsuga' })).toHaveValue(users[1].id)
})

test.each([
  { värde: '', beskrivning: 'tomt' },
  { värde: '0', beskrivning: 'under 1' },
  { värde: '100', beskrivning: 'över 99' },
  { värde: '1.5', beskrivning: 'decimaltal' },
])('ogiltigt poängvärde ($beskrivning) blockerar submit', async ({ värde }) => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = mockUsersAndTasks(users, [])
  render(<App />)

  await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2))
  fireEvent.click(screen.getByRole('button', { name: 'Ny uppgift' }))
  fireEvent.change(screen.getByLabelText('Titel'), { target: { value: 'Dammsuga' } })
  fireEvent.change(screen.getByLabelText('Poäng'), { target: { value: värde } })
  fireEvent.click(screen.getByRole('button', { name: 'Skapa uppgift' }))

  expect(await screen.findByRole('alert')).toHaveTextContent(
    'Poäng måste vara ett heltal mellan 1 och 99.',
  )
  expect(fetchMock).toHaveBeenCalledTimes(2)
})

test('formulärdata bevaras när skapande av uppgift misslyckas', async () => {
  window.localStorage.setItem('hemhub.activeUserId', users[0].id)
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(users))
  fetchMock.mockResolvedValueOnce(jsonResponse([]))
  fetchMock.mockResolvedValueOnce(
    jsonResponse({ code: 'INVALID_TASK', message: 'Uppgiften är ogiltig.' }, 400),
  )

  render(<App />)

  await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2))
  fireEvent.click(screen.getByRole('button', { name: 'Ny uppgift' }))
  const title = screen.getByLabelText('Titel')
  const description = screen.getByLabelText('Beskrivning (valfri)')
  const points = screen.getByLabelText('Poäng')
  const assignee = screen.getByLabelText('Tilldela')
  fireEvent.change(title, { target: { value: 'Dammsuga' } })
  fireEvent.change(description, { target: { value: 'Bottenvåningen' } })
  fireEvent.change(points, { target: { value: '7' } })
  fireEvent.change(assignee, { target: { value: users[1].id } })
  fireEvent.click(screen.getByRole('button', { name: 'Skapa uppgift' }))

  expect(await screen.findByRole('alert')).toHaveTextContent('Uppgiften är ogiltig.')
  expect(screen.getByRole('dialog', { name: 'Skapa ny uppgift' })).toBeInTheDocument()
  expect(title).toHaveValue('Dammsuga')
  expect(description).toHaveValue('Bottenvåningen')
  expect(points).toHaveValue(7)
  expect(assignee).toHaveValue(users[1].id)
})

function mockUsersAndTasks(userResponse: unknown, taskResponse: unknown) {
  const fetchMock = vi.spyOn(globalThis, 'fetch')
  fetchMock.mockResolvedValueOnce(jsonResponse(userResponse))
  fetchMock.mockResolvedValueOnce(jsonResponse(taskResponse))
  return fetchMock
}

function dropTask(taskId: string, status: string) {
  if (!dragAndDrop.onTaskDrop) {
    throw new Error('Drag-and-drop-providern är inte monterad')
  }

  dragAndDrop.onTaskDrop(taskId, status)
}

function mockJsonResponse(body: unknown, status = 200) {
  return vi.spyOn(globalThis, 'fetch').mockResolvedValue(jsonResponse(body, status))
}

function jsonResponse(body: unknown, status = 200) {
  return new Response(JSON.stringify(body), {
    status,
    headers: { 'Content-Type': 'application/json' },
  })
}
