/**
 * Tests for shared Topology visualization components extracted from
 * packages/threat-library into packages/web-components.
 *
 * These tests verify the public API surface of the topology module:
 * node components, edge components, logo lookup, layout builders,
 * styles, and type exports.
 *
 * Expected to FAIL until the module is implemented at ../index.ts
 */
import { describe, it, expect } from 'vitest'
import { createElement } from 'react'
import { render, screen } from '@testing-library/react'

import {
  SourceNode,
  PlatformNode,
  TenantNode,
  ConnectorNode,
  LibraryNode,
  AnimatedEdge,
  getConnectorLogo,
  buildGlobalPipeline,
  buildPipelineForTenant,
  TOPOLOGY_STYLES,
} from '../index'

import type {
  SourceNodeData,
  PlatformNodeData,
  TenantNodeData,
  ConnectorNodeData,
  LibraryNodeData,
  TopologyLayoutData,
} from '../index'

// ---------------------------------------------------------------------------
// 1. Exports availability
// ---------------------------------------------------------------------------

describe('Topology module exports', () => {
  it('exports all five node components as functions', () => {
    expect(typeof SourceNode).toBe('function')
    expect(typeof PlatformNode).toBe('function')
    expect(typeof TenantNode).toBe('function')
    expect(typeof ConnectorNode).toBe('function')
    expect(typeof LibraryNode).toBe('function')
  })

  it('exports AnimatedEdge as a function', () => {
    expect(typeof AnimatedEdge).toBe('function')
  })

  it('exports getConnectorLogo as a function', () => {
    expect(typeof getConnectorLogo).toBe('function')
  })

  it('exports layout builder functions', () => {
    expect(typeof buildGlobalPipeline).toBe('function')
    expect(typeof buildPipelineForTenant).toBe('function')
  })

  it('exports TOPOLOGY_STYLES as a non-empty string', () => {
    expect(typeof TOPOLOGY_STYLES).toBe('string')
    expect(TOPOLOGY_STYLES.length).toBeGreaterThan(0)
    // Should contain the edge animation keyframes
    expect(TOPOLOGY_STYLES).toContain('topology-edge-animated')
    expect(TOPOLOGY_STYLES).toContain('topology-dash')
  })
})

// ---------------------------------------------------------------------------
// 2. Layout builder tests
// ---------------------------------------------------------------------------

describe('buildGlobalPipeline', () => {
  // Sample data matching the shapes from the original threat-library mock
  const sampleSources: TopologyLayoutData['sources'] = [
    {
      name: 'SigmaHQ',
      icon: 'FileCode2',
      cadence: 'Every 6h',
      ruleCount: 2847,
      health: 'healthy' as const,
    },
    {
      name: 'YARA Rules',
      icon: 'Shield',
      cadence: 'Every 12h',
      ruleCount: 412,
      health: 'healthy' as const,
    },
  ]

  const sampleTenants: TopologyLayoutData['tenants'] = [
    {
      name: 'Acme Corp',
      color: '#f97316',
      status: 'active' as const,
      requests24h: 14200,
      detections: 892,
      connectors: [
        { name: 'Splunk SIEM', type: 'siem' as const, status: 'connected' as const },
        { name: 'CrowdStrike Falcon', type: 'edr' as const, status: 'connected' as const },
      ],
    },
  ]

  const sampleLibraryStats: TopologyLayoutData['libraryStats'] = {
    totalDetections: 3348,
    actorsTracked: 245,
    intelReports: 1203,
    techniquesCataloged: 156,
    techniquesTotal: 201,
    classificationBacklog: 42,
  }

  it('returns an object with nodes and edges arrays', () => {
    const result = buildGlobalPipeline({
      sources: sampleSources,
      tenants: sampleTenants,
      libraryStats: sampleLibraryStats,
    })

    expect(result).toHaveProperty('nodes')
    expect(result).toHaveProperty('edges')
    expect(Array.isArray(result.nodes)).toBe(true)
    expect(Array.isArray(result.edges)).toBe(true)
  })

  it('produces nodes with the expected type values', () => {
    const result = buildGlobalPipeline({
      sources: sampleSources,
      tenants: sampleTenants,
      libraryStats: sampleLibraryStats,
    })

    const nodeTypes = new Set(result.nodes.map(n => n.type))

    // Must include all these node types
    expect(nodeTypes.has('source')).toBe(true)
    expect(nodeTypes.has('platform')).toBe(true)
    expect(nodeTypes.has('library')).toBe(true)
    expect(nodeTypes.has('tenant')).toBe(true)
    expect(nodeTypes.has('connector')).toBe(true)
    expect(nodeTypes.has('columnLabel')).toBe(true)
  })

  it('creates one source node per source entry', () => {
    const result = buildGlobalPipeline({
      sources: sampleSources,
      tenants: sampleTenants,
      libraryStats: sampleLibraryStats,
    })

    const sourceNodes = result.nodes.filter(n => n.type === 'source')
    expect(sourceNodes).toHaveLength(sampleSources.length)
  })

  it('creates one tenant node per tenant entry', () => {
    const result = buildGlobalPipeline({
      sources: sampleSources,
      tenants: sampleTenants,
      libraryStats: sampleLibraryStats,
    })

    const tenantNodes = result.nodes.filter(n => n.type === 'tenant')
    expect(tenantNodes).toHaveLength(sampleTenants.length)
  })

  it('creates connector nodes for each tenant connector', () => {
    const result = buildGlobalPipeline({
      sources: sampleSources,
      tenants: sampleTenants,
      libraryStats: sampleLibraryStats,
    })

    const connectorNodes = result.nodes.filter(n => n.type === 'connector')
    const expectedCount = sampleTenants.reduce((sum, t) => sum + t.connectors.length, 0)
    expect(connectorNodes).toHaveLength(expectedCount)
  })

  it('generates edges connecting sources to the platform pipeline', () => {
    const result = buildGlobalPipeline({
      sources: sampleSources,
      tenants: sampleTenants,
      libraryStats: sampleLibraryStats,
    })

    const pipelineEdges = result.edges.filter(e => e.target === 'platform-pipeline')
    expect(pipelineEdges.length).toBeGreaterThanOrEqual(sampleSources.length)
  })

  it('creates exactly one platform pipeline node', () => {
    const result = buildGlobalPipeline({
      sources: sampleSources,
      tenants: sampleTenants,
      libraryStats: sampleLibraryStats,
    })

    const platformNodes = result.nodes.filter(n => n.type === 'platform')
    expect(platformNodes).toHaveLength(1)
  })

  it('creates five column label nodes', () => {
    const result = buildGlobalPipeline({
      sources: sampleSources,
      tenants: sampleTenants,
      libraryStats: sampleLibraryStats,
    })

    const labelNodes = result.nodes.filter(n => n.type === 'columnLabel')
    expect(labelNodes).toHaveLength(5)

    const labels = labelNodes.map(n => (n.data as Record<string, unknown>).label)
    expect(labels).toContain('Sources')
    expect(labels).toContain('Pipeline')
    expect(labels).toContain('Threat Library')
    expect(labels).toContain('Tenants')
    expect(labels).toContain('Connectors')
  })
})

describe('buildPipelineForTenant', () => {
  const sources: TopologyLayoutData['sources'] = [
    {
      name: 'SigmaHQ',
      icon: 'FileCode2',
      cadence: 'Every 6h',
      ruleCount: 2847,
      health: 'healthy' as const,
    },
    {
      name: 'YARA Rules',
      icon: 'Shield',
      cadence: 'Every 12h',
      ruleCount: 412,
      health: 'healthy' as const,
    },
  ]

  const tenants: TopologyLayoutData['tenants'] = [
    {
      name: 'Acme Corp',
      color: '#f97316',
      status: 'active' as const,
      requests24h: 14200,
      detections: 892,
      connectors: [{ name: 'Splunk SIEM', type: 'siem' as const, status: 'connected' as const }],
    },
    {
      name: 'FinServ Global',
      color: '#3b82f6',
      status: 'degraded' as const,
      requests24h: 8100,
      detections: 430,
      connectors: [
        { name: 'CrowdStrike Falcon', type: 'edr' as const, status: 'connected' as const },
        { name: 'Elastic SIEM', type: 'siem' as const, status: 'degraded' as const },
      ],
    },
  ]

  const libraryStats: TopologyLayoutData['libraryStats'] = {
    totalDetections: 3348,
    actorsTracked: 245,
    intelReports: 1203,
    techniquesCataloged: 156,
    techniquesTotal: 201,
    classificationBacklog: 42,
  }

  // Mapping of which sources each tenant subscribes to
  const tenantSourceMap: TopologyLayoutData['tenantSourceMap'] = {
    'Acme Corp': ['SigmaHQ', 'YARA Rules'],
    'FinServ Global': ['SigmaHQ'],
  }

  const tenantLibraryMap: TopologyLayoutData['tenantLibraryMap'] = {
    'Acme Corp': ['lib-detections', 'lib-attack'],
    'FinServ Global': ['lib-detections', 'lib-actors', 'lib-intel'],
  }

  const layoutData: TopologyLayoutData = {
    sources,
    tenants,
    libraryStats,
    tenantSourceMap,
    tenantLibraryMap,
  }

  it('returns nodes and edges for a valid tenant name', () => {
    const result = buildPipelineForTenant('Acme Corp', layoutData)

    expect(result.nodes.length).toBeGreaterThan(0)
    expect(result.edges.length).toBeGreaterThan(0)
  })

  it('returns only nodes relevant to the selected tenant', () => {
    const result = buildPipelineForTenant('Acme Corp', layoutData)

    const tenantNodes = result.nodes.filter(n => n.type === 'tenant')
    expect(tenantNodes).toHaveLength(1)
    expect((tenantNodes[0]!.data as Record<string, unknown>).name).toBe('Acme Corp')
  })

  it('includes only connectors belonging to the selected tenant', () => {
    const result = buildPipelineForTenant('Acme Corp', layoutData)

    const connectorNodes = result.nodes.filter(n => n.type === 'connector')
    // Acme Corp has 1 connector: Splunk SIEM
    expect(connectorNodes).toHaveLength(1)
  })

  it('returns empty nodes and edges for a non-existent tenant', () => {
    const result = buildPipelineForTenant('NonExistent Corp', layoutData)

    expect(result.nodes).toHaveLength(0)
    expect(result.edges).toHaveLength(0)
  })

  it('includes source nodes that the selected tenant subscribes to', () => {
    const result = buildPipelineForTenant('Acme Corp', layoutData)

    const sourceNodes = result.nodes.filter(n => n.type === 'source')
    const sourceNames = sourceNodes.map(n => (n.data as Record<string, unknown>).name)

    // Acme Corp subscribes to SigmaHQ and YARA Rules
    expect(sourceNames).toContain('SigmaHQ')
    expect(sourceNames).toContain('YARA Rules')
    expect(sourceNodes).toHaveLength(2)
  })

  it('includes library nodes the tenant subscribes to', () => {
    const result = buildPipelineForTenant('Acme Corp', layoutData)

    const libraryNodes = result.nodes.filter(n => n.type === 'library')
    // Acme Corp subscribes to lib-detections and lib-attack
    expect(libraryNodes).toHaveLength(2)
  })

  it('reflects degraded status in edge data when tenant is degraded', () => {
    const result = buildPipelineForTenant('FinServ Global', layoutData)

    // Edges from library to tenant should have degraded status
    const libToTenantEdges = result.edges.filter(
      e => e.target === 'tenant-FinServ Global' && e.source?.startsWith('lib-')
    )

    expect(libToTenantEdges.length).toBeGreaterThan(0)
    for (const edge of libToTenantEdges) {
      expect((edge.data as Record<string, unknown>).status).toBe('degraded')
    }
  })
})

// ---------------------------------------------------------------------------
// 3. Logo lookup tests
// ---------------------------------------------------------------------------

describe('getConnectorLogo', () => {
  it('returns a React component for "Splunk SIEM"', () => {
    const Logo = getConnectorLogo('Splunk SIEM')
    expect(typeof Logo).toBe('function')

    // Should render without throwing
    const { container } = render(createElement(Logo, { size: 16 }))
    expect(container.querySelector('svg')).not.toBeNull()
  })

  it('returns CrowdStrikeLogo for "CrowdStrike Falcon"', () => {
    const Logo = getConnectorLogo('CrowdStrike Falcon')
    expect(typeof Logo).toBe('function')

    const { container } = render(createElement(Logo, { size: 16 }))
    const svg = container.querySelector('svg')
    expect(svg).not.toBeNull()
    // CrowdStrike logo uses the red fill color #FC0000
    expect(svg?.innerHTML).toContain('#FC0000')
  })

  it('returns DefaultConnectorLogo for an unknown connector name', () => {
    const Logo = getConnectorLogo('unknown')
    expect(typeof Logo).toBe('function')

    const { container } = render(createElement(Logo, { size: 16 }))
    expect(container.querySelector('svg')).not.toBeNull()
  })

  it('returns a component for "Google Chronicle"', () => {
    const Logo = getConnectorLogo('Google Chronicle')
    expect(typeof Logo).toBe('function')
  })

  it('returns a component for "Microsoft Sentinel"', () => {
    const Logo = getConnectorLogo('Microsoft Sentinel')
    expect(typeof Logo).toBe('function')
  })

  it('returns MitreLogo for "MITRE ATT&CK"', () => {
    const Logo = getConnectorLogo('MITRE ATT&CK')
    expect(typeof Logo).toBe('function')

    const { container } = render(createElement(Logo, { size: 16 }))
    const svg = container.querySelector('svg')
    expect(svg).not.toBeNull()
    // MITRE logo uses the red fill color #fa243c
    expect(svg?.innerHTML).toContain('#fa243c')
  })

  it('handles case-insensitive lookup', () => {
    const upper = getConnectorLogo('SPLUNK SIEM')
    const lower = getConnectorLogo('splunk siem')
    const mixed = getConnectorLogo('Splunk SIEM')

    // All should resolve to the same component (SplunkLogo)
    expect(upper).toBe(lower)
    expect(lower).toBe(mixed)
  })
})

// ---------------------------------------------------------------------------
// 4. Node rendering tests
// ---------------------------------------------------------------------------

describe('SourceNode', () => {
  const defaultData: SourceNodeData = {
    name: 'SigmaHQ',
    icon: 'FileCode2',
    category: 'detection',
    categoryLabel: 'Detection Source',
    health: 'healthy',
    ruleCount: 2847,
    countLabel: 'rules',
    cadence: 'Every 6h',
  }

  it('renders without crashing', () => {
    // SourceNode expects NodeProps shape from @xyflow/react
    const { container } = render(
      createElement(SourceNode, {
        id: 'src-sigma',
        type: 'source',
        data: defaultData,
        // Minimal xyflow NodeProps stubs
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(container.firstChild).not.toBeNull()
  })

  it('renders the source name', () => {
    render(
      createElement(SourceNode, {
        id: 'src-sigma',
        type: 'source',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(screen.getByText('SigmaHQ')).toBeDefined()
  })

  it('renders the rule count', () => {
    render(
      createElement(SourceNode, {
        id: 'src-sigma',
        type: 'source',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    // 2847 formatted with locale => "2,847"
    expect(screen.getByText('2,847')).toBeDefined()
  })
})

describe('PlatformNode', () => {
  const defaultData: PlatformNodeData = {
    name: 'Collection Pipeline',
    role: 'Processing',
    metric: 'Healthy',
  }

  it('renders without crashing', () => {
    const { container } = render(
      createElement(PlatformNode, {
        id: 'platform-pipeline',
        type: 'platform',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(container.firstChild).not.toBeNull()
  })

  it('renders the platform name and role', () => {
    render(
      createElement(PlatformNode, {
        id: 'platform-pipeline',
        type: 'platform',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(screen.getByText('Collection Pipeline')).toBeDefined()
    expect(screen.getByText('Processing')).toBeDefined()
  })
})

describe('TenantNode', () => {
  const defaultData: TenantNodeData = {
    name: 'Acme Corp',
    color: '#f97316',
    status: 'active',
    requests24h: 14200,
    detections: 892,
  }

  it('renders without crashing', () => {
    const { container } = render(
      createElement(TenantNode, {
        id: 'tenant-acme',
        type: 'tenant',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(container.firstChild).not.toBeNull()
  })

  it('renders the tenant name', () => {
    render(
      createElement(TenantNode, {
        id: 'tenant-acme',
        type: 'tenant',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(screen.getByText('Acme Corp')).toBeDefined()
  })

  it('renders the tenant status label', () => {
    render(
      createElement(TenantNode, {
        id: 'tenant-acme',
        type: 'tenant',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(screen.getByText('Active')).toBeDefined()
  })

  it('renders requests and detections metrics', () => {
    render(
      createElement(TenantNode, {
        id: 'tenant-acme',
        type: 'tenant',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    // 14200 formatted with locale => "14,200"
    expect(screen.getByText('14,200')).toBeDefined()
    expect(screen.getByText('892')).toBeDefined()
  })
})

describe('ConnectorNode', () => {
  const defaultData: ConnectorNodeData = {
    name: 'Splunk SIEM',
    type: 'siem',
    status: 'connected',
  }

  it('renders without crashing', () => {
    const { container } = render(
      createElement(ConnectorNode, {
        id: 'conn-splunk',
        type: 'connector',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(container.firstChild).not.toBeNull()
  })

  it('renders the connector name', () => {
    render(
      createElement(ConnectorNode, {
        id: 'conn-splunk',
        type: 'connector',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(screen.getByText('Splunk SIEM')).toBeDefined()
  })

  it('renders the connector type label', () => {
    render(
      createElement(ConnectorNode, {
        id: 'conn-splunk',
        type: 'connector',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(screen.getByText('SIEM')).toBeDefined()
  })
})

describe('LibraryNode', () => {
  const defaultData: LibraryNodeData = {
    name: 'Detection Library',
    category: 'detection',
    metric: '3,348 rules',
  }

  it('renders without crashing', () => {
    const { container } = render(
      createElement(LibraryNode, {
        id: 'lib-detections',
        type: 'library',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(container.firstChild).not.toBeNull()
  })

  it('renders the library name and metric', () => {
    render(
      createElement(LibraryNode, {
        id: 'lib-detections',
        type: 'library',
        data: defaultData,
        dragging: false,
        zIndex: 0,
        isConnectable: true,
        positionAbsoluteX: 0,
        positionAbsoluteY: 0,
      } as any)
    )

    expect(screen.getByText('Detection Library')).toBeDefined()
    expect(screen.getByText('3,348 rules')).toBeDefined()
  })
})
