import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { MemoryRouter } from 'react-router-dom'
import React from 'react'
import { MoveRubroDialog } from '../../../features/rubros/components/MoveRubroDialog'
import { flattenExcludingSubtree } from '../../../features/rubros/components/MoveRubroDialog'
import type { Rubro, RubroTreeNode } from '../../../features/rubros/types'

vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))

// Mock useMoveRubro to avoid real network calls
const mockMutateAsync = vi.fn()
let mockIsPending = false

vi.mock('../../../features/rubros/hooks/useMoveRubro', () => ({
  useMoveRubro: () => ({
    mutateAsync: mockMutateAsync,
    get isPending() {
      return mockIsPending
    },
  }),
}))

// ─── Fixtures ─────────────────────────────────────────────────────────────────

const rubroAutos: Rubro = {
  id: 1,
  nombre: 'Autos',
  orden: 1,
  activo: true,
  parentId: null,
  tarifarioBaseId: null,
  fechaCreacion: '2026-04-18T00:00:00Z',
  fechaModificacion: null,
}

const rubroUsados: Rubro = {
  id: 2,
  nombre: 'Usados',
  orden: 1,
  activo: true,
  parentId: 1,
  tarifarioBaseId: null,
  fechaCreacion: '2026-04-18T00:00:00Z',
  fechaModificacion: null,
}

const rubroInmuebles: Rubro = {
  id: 3,
  nombre: 'Inmuebles',
  orden: 2,
  activo: true,
  parentId: null,
  tarifarioBaseId: null,
  fechaCreacion: '2026-04-18T00:00:00Z',
  fechaModificacion: null,
}

// Tree:
// Autos (id=1)
//   Usados (id=2)
//     Compactos (id=4)
// Inmuebles (id=3)
const treeCompact: RubroTreeNode = {
  id: 4,
  nombre: 'Compactos',
  orden: 1,
  activo: true,
  parentId: 2,
  tarifarioBaseId: null,
  hijos: [],
}

const treeUsados: RubroTreeNode = {
  id: 2,
  nombre: 'Usados',
  orden: 1,
  activo: true,
  parentId: 1,
  tarifarioBaseId: null,
  hijos: [treeCompact],
}

const treeAutos: RubroTreeNode = {
  id: 1,
  nombre: 'Autos',
  orden: 1,
  activo: true,
  parentId: null,
  tarifarioBaseId: null,
  hijos: [treeUsados],
}

const treeInmuebles: RubroTreeNode = {
  id: 3,
  nombre: 'Inmuebles',
  orden: 2,
  activo: true,
  parentId: null,
  tarifarioBaseId: null,
  hijos: [],
}

const fullTree: RubroTreeNode[] = [treeAutos, treeInmuebles]

// ─── Helper ───────────────────────────────────────────────────────────────────

function wrap(children: React.ReactNode) {
  const qc = new QueryClient({
    defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
  })
  return render(
    <QueryClientProvider client={qc}>
      <MemoryRouter>{children}</MemoryRouter>
    </QueryClientProvider>,
  )
}

// ─── flattenExcludingSubtree unit tests ───────────────────────────────────────

describe('flattenExcludingSubtree', () => {
  it('returns all nodes when excludedId does not match anything', () => {
    const result = flattenExcludingSubtree(fullTree, 999)
    expect(result.map((n) => n.id)).toContain(1)
    expect(result.map((n) => n.id)).toContain(2)
    expect(result.map((n) => n.id)).toContain(3)
    expect(result.map((n) => n.id)).toContain(4)
  })

  it('excludes the target node itself', () => {
    const result = flattenExcludingSubtree(fullTree, 1)
    expect(result.map((n) => n.id)).not.toContain(1)
  })

  it('excludes all descendants of the target node', () => {
    const result = flattenExcludingSubtree(fullTree, 1)
    // Autos (id=1), Usados (id=2), Compactos (id=4) all excluded
    expect(result.map((n) => n.id)).not.toContain(1)
    expect(result.map((n) => n.id)).not.toContain(2)
    expect(result.map((n) => n.id)).not.toContain(4)
    // Inmuebles stays
    expect(result.map((n) => n.id)).toContain(3)
  })

  it('excludes only the leaf node when a leaf is the target', () => {
    const result = flattenExcludingSubtree(fullTree, 4)
    expect(result.map((n) => n.id)).toContain(1)
    expect(result.map((n) => n.id)).toContain(2)
    expect(result.map((n) => n.id)).toContain(3)
    expect(result.map((n) => n.id)).not.toContain(4)
  })
})

// ─── MoveRubroDialog component tests ─────────────────────────────────────────

describe('MoveRubroDialog', () => {
  beforeEach(() => {
    mockMutateAsync.mockReset()
    mockIsPending = false
  })

  it('renders with current parent selected when opened', async () => {
    // rubroUsados has parentId=1 (Autos)
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroUsados}
        tree={fullTree}
      />,
    )
    await waitFor(() => {
      expect(screen.getByRole('dialog')).toBeInTheDocument()
    })
    expect(screen.getByText(/mover rubro/i)).toBeInTheDocument()
  })

  it('shows dialog title with rubro name', async () => {
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroAutos}
        tree={fullTree}
      />,
    )
    await waitFor(() => {
      expect(screen.getByText(/mover rubro/i)).toBeInTheDocument()
    })
  })

  it('shows flat list of available parents excluding the rubro being moved', async () => {
    // Moving Inmuebles (id=3) — Inmuebles should not appear as option, others should
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroInmuebles}
        tree={fullTree}
      />,
    )
    await waitFor(() => {
      expect(screen.getByRole('dialog')).toBeInTheDocument()
    })
    // The combobox/select area should contain Autos and Usados but not Inmuebles
    // We check via the select trigger content after opening
    const trigger = screen.getByRole('combobox')
    expect(trigger).toBeInTheDocument()
  })

  it('shows flat list of available parents excluding DESCENDANTS of the rubro being moved', async () => {
    // Moving Autos (id=1) — Usados (id=2) and Compactos (id=4) are descendants
    // The options should not include Autos, Usados, or Compactos
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroAutos}
        tree={fullTree}
      />,
    )
    await waitFor(() => {
      expect(screen.getByRole('dialog')).toBeInTheDocument()
    })
    // Open the select
    const trigger = screen.getByRole('combobox')
    await userEvent.click(trigger)
    await waitFor(() => {
      // Inmuebles should be available as an option
      expect(screen.getByRole('option', { name: /inmuebles/i })).toBeInTheDocument()
    })
    // Autos itself and its descendants should not appear
    expect(screen.queryByRole('option', { name: /^autos$/i })).not.toBeInTheDocument()
    expect(screen.queryByRole('option', { name: /usados/i })).not.toBeInTheDocument()
    expect(screen.queryByRole('option', { name: /compactos/i })).not.toBeInTheDocument()
  })

  it('allows selecting "raíz" (null parent)', async () => {
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroUsados}
        tree={fullTree}
      />,
    )
    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
    const trigger = screen.getByRole('combobox')
    await userEvent.click(trigger)
    await waitFor(() => {
      expect(screen.getByRole('option', { name: /raíz/i })).toBeInTheDocument()
    })
  })

  it('submit calls mutateAsync with { nuevoParentId, nuevoOrden }', async () => {
    mockMutateAsync.mockResolvedValue(undefined)
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroUsados}
        tree={fullTree}
        onConfirmed={vi.fn()}
      />,
    )
    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())

    // Select raíz
    const trigger = screen.getByRole('combobox')
    await userEvent.click(trigger)
    await waitFor(() => expect(screen.getByRole('option', { name: /raíz/i })).toBeInTheDocument())
    await userEvent.click(screen.getByRole('option', { name: /raíz/i }))

    // Set orden
    const ordenInput = screen.getByLabelText(/orden/i)
    await userEvent.clear(ordenInput)
    await userEvent.type(ordenInput, '5')

    // Submit
    await userEvent.click(screen.getByRole('button', { name: /mover/i }))

    await waitFor(() => {
      expect(mockMutateAsync).toHaveBeenCalledWith({
        id: rubroUsados.id,
        data: { nuevoParentId: null, nuevoOrden: 5 },
      })
    })
  })

  it('displays backend error inline when 409 cycle/duplicate', async () => {
    mockMutateAsync.mockRejectedValue({
      response: {
        status: 409,
        data: { message: 'Ya existe un rubro con ese nombre en el destino' },
      },
    })
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroUsados}
        tree={fullTree}
      />,
    )
    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())

    await userEvent.click(screen.getByRole('button', { name: /mover/i }))

    await waitFor(() => {
      expect(screen.getByText(/ya existe un rubro con ese nombre/i)).toBeInTheDocument()
    })
  })

  it('displays backend error inline when 409 rubro_padre_es_hoja_con_avisos (CAT-002)', async () => {
    mockMutateAsync.mockRejectedValue({
      response: {
        status: 409,
        data: {
          error: 'rubro_padre_es_hoja_con_avisos',
          message: 'El rubro padre contiene 3 avisos. Muévalos antes de crear sub-rubros.',
        },
      },
    })
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroUsados}
        tree={fullTree}
      />,
    )
    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())

    await userEvent.click(screen.getByRole('button', { name: /mover/i }))

    await waitFor(() => {
      expect(
        screen.getByText(/el rubro padre contiene 3 avisos/i),
      ).toBeInTheDocument()
    })
  })

  it('displays backend error inline when 422 depth', async () => {
    mockMutateAsync.mockRejectedValue({
      response: {
        status: 422,
        data: { message: 'Profundidad máxima 10 niveles alcanzada' },
      },
    })
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroUsados}
        tree={fullTree}
      />,
    )
    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())

    await userEvent.click(screen.getByRole('button', { name: /mover/i }))

    await waitFor(() => {
      expect(screen.getByText(/profundidad máxima/i)).toBeInTheDocument()
    })
  })

  it('disables submit button while mutation is pending', async () => {
    // Set isPending to true before rendering — simulates pending state
    mockIsPending = true
    mockMutateAsync.mockImplementation(() => new Promise(() => {}))

    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={vi.fn()}
        rubro={rubroUsados}
        tree={fullTree}
      />,
    )
    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())

    // When isPending=true, button shows "Moviendo..." and is disabled
    const submitBtn = screen.getByRole('button', { name: /moviendo/i })
    expect(submitBtn).toBeDisabled()
  })

  it('closes on cancel', async () => {
    const onOpenChange = vi.fn()
    wrap(
      <MoveRubroDialog
        open={true}
        onOpenChange={onOpenChange}
        rubro={rubroUsados}
        tree={fullTree}
      />,
    )
    await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument())
    await userEvent.click(screen.getByRole('button', { name: /cancelar/i }))
    expect(onOpenChange).toHaveBeenCalledWith(false)
  })
})
