import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { Server } from 'lucide-react'
import { BaseNode } from '../nodes/BaseNode'
import type { NodeData } from '@/types'
import type { Node } from '@xyflow/react'

let mockZoom = 1

vi.mock('@xyflow/react', () => ({
  Handle: () => null,
  Position: { Top: 'top', Bottom: 'bottom', Left: 'left', Right: 'right' },
  NodeResizer: () => null,
  useUpdateNodeInternals: () => vi.fn(),
  useViewport: () => ({ zoom: mockZoom }),
}))

vi.mock('@/stores/themeStore', () => ({
  useThemeStore: (sel: (s: { activeTheme: string }) => unknown) => sel({ activeTheme: 'dark' }),
}))

vi.mock('@/stores/canvasStore', () => ({
  useCanvasStore: (sel: (s: { hideIp: boolean; serviceStatuses: Record<string, string> }) => unknown) =>
    sel({ hideIp: false, serviceStatuses: {} }),
  serviceStatusKey: (nodeId: string, port?: number, protocol?: string) => `${nodeId}:${port ?? ''}/${protocol ?? ''}`,
}))

vi.mock('@/utils/themes', () => ({
  THEMES: {
    dark: {
      colors: {
        statusColors: { online: '#39d353', offline: '#f85149', pending: '#e3b341', unknown: '#8b949e' },
        nodeSubtextColor: '#8b949e',
        nodeLabelColor: '#e6edf3',
        nodeIconBackground: '#21262d',
        handleBackground: '#30363d',
        handleBorder: '#30363d',
      },
    },
  },
}))

vi.mock('@/utils/nodeColors', () => ({
  resolveNodeColors: () => ({ background: '#161b22', border: '#30363d', icon: '#00d4ff' }),
}))

vi.mock('@/utils/nodeIcons', () => ({
  resolveNodeIcon: (_typeIcon: unknown) => _typeIcon,
  isBrandIconKey: (k: string | undefined) => !!k && k.startsWith('brand:'),
}))

vi.mock('@/utils/maskIp', () => ({
  maskIp: (ip: string) => ip,
  splitIps: (ip: string) => ip ? ip.split(',').map((s: string) => s.trim()).filter(Boolean) : [],
  primaryIp: (ip: string) => ip ? ip.split(',')[0].trim() : '',
}))

vi.mock('@/utils/propertyIcons', () => ({
  resolvePropertyIcon: (icon: string | null) => icon ? Server : null,
}))

// handleUtils is pure — use the real implementation so the side-generic API
// (SIDES, handleId, handlePositions, sideHandleCount, …) stays in sync.
vi.mock('@/utils/handleUtils', async (importOriginal) => await importOriginal())

beforeEach(() => { mockZoom = 1 })

function makeNode(data: Partial<NodeData>): Node<NodeData> {
  return {
    id: 'n1',
    type: data.type ?? 'server',
    position: { x: 0, y: 0 },
    data: {
      label: 'Test Node',
      type: 'server',
      status: 'online',
      services: [],
      ...data,
    },
  }
}

function renderBaseNode(data: Partial<NodeData>) {
  const node = makeNode(data)
  return render(
    <BaseNode
      id={node.id}
      data={node.data}
      selected={false}
      icon={Server}
      type="server"
      dragging={false}
      zIndex={0}
      isConnectable={true}
      positionAbsoluteX={0}
      positionAbsoluteY={0}
    />
  )
}

describe('BaseNode — borderWidth zoom scaling', () => {
  beforeEach(() => { mockZoom = 1 })

  it('borderWidth is 1px at zoom=1', () => {
    mockZoom = 1
    const { container } = renderBaseNode({})
    expect((container.firstChild as HTMLElement).style.borderWidth).toBe('1px')
  })

  it('borderWidth scales to 2px at zoom=0.5', () => {
    mockZoom = 0.5
    const { container } = renderBaseNode({})
    expect((container.firstChild as HTMLElement).style.borderWidth).toBe('2px')
  })

  it('borderWidth is clamped to 1px at zoom=2', () => {
    mockZoom = 2
    const { container } = renderBaseNode({})
    expect((container.firstChild as HTMLElement).style.borderWidth).toBe('1px')
  })

  // Regression: the node root must NOT clip its own bounds, otherwise the outer
  // half of each connection handle (which sits centred on the edge) becomes
  // non-interactive and the "magnet" snap area is halved.
  it('root does not clip overflow (keeps handles grabbable)', () => {
    const { container } = renderBaseNode({})
    expect((container.firstChild as HTMLElement).className).not.toContain('overflow-hidden')
  })

  it('boxShadow glow ring uses borderWidth when selected + online at zoom=0.5', () => {
    mockZoom = 0.5
    const node = makeNode({ status: 'online' })
    const { container } = render(
      <BaseNode id={node.id} data={node.data} selected={true} icon={Server}
        type="server" dragging={false} zIndex={0} isConnectable={true}
        positionAbsoluteX={0} positionAbsoluteY={0} />
    )
    expect((container.firstChild as HTMLElement).style.boxShadow).toContain('0 0 0 2px')
  })
})

describe('BaseNode — properties rendering', () => {
  it('renders visible properties on the node', () => {
    renderBaseNode({
      properties: [
        { key: 'CPU Model', value: 'i7-12700K', icon: 'Cpu', visible: true },
        { key: 'RAM', value: '32 GB', icon: 'MemoryStick', visible: true },
      ],
    })
    expect(screen.getByText('CPU Model')).toBeDefined()
    // Value is rendered with a middle-dot prefix: "· 32 GB"
    expect(screen.getByText(/32 GB/)).toBeDefined()
  })

  it('does not render properties with visible=false', () => {
    renderBaseNode({
      properties: [
        { key: 'Secret', value: 'hidden', icon: null, visible: false },
      ],
    })
    expect(screen.queryByText('Secret')).toBeNull()
  })

  it('renders nothing when properties array is empty', () => {
    const { container } = renderBaseNode({ properties: [] })
    // No properties section — only the main node card
    expect(container.querySelectorAll('.flex.flex-col.gap-1').length).toBe(0)
  })

  it('renders label and ip regardless of properties', () => {
    renderBaseNode({
      label: 'My Server',
      ip: '192.168.1.10',
      properties: [{ key: 'OS', value: 'Debian 12', icon: 'Server', visible: true }],
    })
    expect(screen.getByText('My Server')).toBeDefined()
    expect(screen.getByText('192.168.1.10')).toBeDefined()
    expect(screen.getByText('OS')).toBeDefined()
  })
})

describe('BaseNode — port numbers (issue #20 / #243)', () => {
  it('renders a number on each connection point when show_port_numbers is on', () => {
    // bottom=4 → labels 1..4; top defaults to 1 → an extra "1" on top.
    renderBaseNode({ bottom_handles: 4, show_port_numbers: true })
    expect(screen.getAllByText('1')).toHaveLength(2) // top slot 0 + bottom slot 0
    expect(screen.getByText('2')).toBeDefined()
    expect(screen.getByText('3')).toBeDefined()
    expect(screen.getByText('4')).toBeDefined()
  })

  it('labels left/right connection points too when enabled', () => {
    // top=1, bottom=1, left=2, right=0 → labels: two "1" (top+bottom) + "2" (left).
    renderBaseNode({ bottom_handles: 1, left_handles: 2, show_port_numbers: true })
    expect(screen.getByText('2')).toBeDefined()
    expect(screen.getAllByText('1')).toHaveLength(3) // top + bottom + left slot 0
  })

  it('does not render port numbers when show_port_numbers is off', () => {
    renderBaseNode({ bottom_handles: 4 })
    expect(screen.queryByText('1')).toBeNull()
    expect(screen.queryByText('4')).toBeNull()
  })

  it('numbers match the handle count', () => {
    // bottom=2 → 1,2; top default 1 adds one more "1"; no "3".
    renderBaseNode({ bottom_handles: 2, show_port_numbers: true })
    expect(screen.getAllByText('1')).toHaveLength(2)
    expect(screen.getByText('2')).toBeDefined()
    expect(screen.queryByText('3')).toBeNull()
  })
})

describe('BaseNode — services visibility toggle', () => {
  it('does not render service toggle button on the node', () => {
    renderBaseNode({ services: [{ service_name: 'nginx', port: 80, protocol: 'tcp' }] })
    expect(screen.queryByTitle('Show services')).toBeNull()
  })

  it('renders service rows when services are toggled on', () => {
    renderBaseNode({
      ip: '192.168.1.10',
      custom_colors: { show_services: true },
      services: [
        { service_name: 'nginx', port: 80, protocol: 'tcp' },
        { service_name: 'ssh', port: 22, protocol: 'tcp' },
      ],
    })

    expect(screen.getByText('nginx')).toBeDefined()
    expect(screen.getByText('80')).toBeDefined()
    expect(screen.getByText('ssh')).toBeDefined()
  })

  it('renders clickable service links for web services', () => {
    renderBaseNode({
      ip: '192.168.1.10',
      custom_colors: { show_services: true },
      services: [{ service_name: 'nginx', port: 80, protocol: 'tcp' }],
    })

    const link = screen.getByRole('link', { name: /nginx/i }) as HTMLAnchorElement
    expect(link.getAttribute('href')).toBe('http://192.168.1.10:80')
  })

  it('keeps non-web services as non-clickable rows', () => {
    renderBaseNode({
      ip: '192.168.1.10',
      custom_colors: { show_services: true },
      services: [{ service_name: 'ssh', port: 22, protocol: 'tcp' }],
    })

    expect(screen.queryByRole('link', { name: /ssh/i })).toBeNull()
  })
})

describe('BaseNode — legacy hardware fallback', () => {
  it('renders legacy hardware when properties is undefined and show_hardware is true', () => {
    renderBaseNode({
      properties: undefined,
      show_hardware: true,
      cpu_model: 'Intel Xeon E5-2680',
      ram_gb: 32,
    })
    expect(screen.getByText('Intel Xeon E5-2680')).toBeDefined()
  })

  it('does not render legacy hardware when properties array is present (even if empty)', () => {
    renderBaseNode({
      properties: [],
      show_hardware: true,
      cpu_model: 'Intel Xeon E5-2680',
    })
    // properties array exists → new system, legacy section skipped
    expect(screen.queryByText('Intel Xeon E5-2680')).toBeNull()
  })

  it('does not render legacy hardware when show_hardware is false', () => {
    renderBaseNode({
      properties: undefined,
      show_hardware: false,
      cpu_model: 'Intel Xeon E5-2680',
    })
    expect(screen.queryByText('Intel Xeon E5-2680')).toBeNull()
  })
})
