import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import client from './client'
import { getLogs, getLogContent, downloadLog, connectLiveLogs, connectSecurityLogs } from './logs'
import type { LiveLogEntry, SecurityLogEntry } from './logs'

vi.mock('./client', () => ({
  default: {
    get: vi.fn(),
  },
}))

const mockedClient = client as unknown as {
  get: ReturnType<typeof vi.fn>
}

class MockWebSocket {
  static CONNECTING = 0
  static OPEN = 1
  static CLOSED = 3
  static instances: MockWebSocket[] = []

  url: string
  readyState = MockWebSocket.CONNECTING
  onopen: (() => void) | null = null
  onmessage: ((event: { data: string }) => void) | null = null
  onerror: ((event: Event) => void) | null = null
  onclose: ((event: CloseEvent) => void) | null = null

  constructor(url: string) {
    this.url = url
    MockWebSocket.instances.push(this)
  }

  open() {
    this.readyState = MockWebSocket.OPEN
    this.onopen?.()
  }

  sendMessage(data: string) {
    this.onmessage?.({ data })
  }

  triggerError(event: Event) {
    this.onerror?.(event)
  }

  close() {
    this.readyState = MockWebSocket.CLOSED
    this.onclose?.({ code: 1000, reason: '', wasClean: true } as CloseEvent)
  }
}

const originalWebSocket = globalThis.WebSocket
const originalLocation = { ...window.location }

beforeEach(() => {
  vi.clearAllMocks()
  ;(globalThis as unknown as { WebSocket: typeof WebSocket }).WebSocket = MockWebSocket as unknown as typeof WebSocket
  Object.defineProperty(window, 'location', {
    value: { ...originalLocation, protocol: 'http:', host: 'localhost', href: '' },
    writable: true,
  })
})

afterEach(() => {
  ;(globalThis as unknown as { WebSocket: typeof WebSocket }).WebSocket = originalWebSocket
  Object.defineProperty(window, 'location', { value: originalLocation })
  MockWebSocket.instances.length = 0
})

describe('logs api', () => {
  it('lists log files', async () => {
    mockedClient.get.mockResolvedValue({ data: [{ name: 'access.log', size: 10, mod_time: 'now' }] })

    const logs = await getLogs()

    expect(mockedClient.get).toHaveBeenCalledWith('/logs')
    expect(logs[0].name).toBe('access.log')
  })

  it('fetches log content with filters applied', async () => {
    mockedClient.get.mockResolvedValue({ data: { filename: 'access.log', logs: [], total: 0, limit: 50, offset: 0 } })

    await getLogContent('access.log', {
      search: 'error',
      host: 'example.com',
      status: '500',
      level: 'error',
      limit: 50,
      offset: 10,
      sort: 'asc',
    })

    expect(mockedClient.get).toHaveBeenCalledWith(
      '/logs/access.log?search=error&host=example.com&status=500&level=error&limit=50&offset=10&sort=asc'
    )
  })

  it('sets window location when downloading logs', () => {
    downloadLog('access.log')
    expect(window.location.href).toBe('/api/v1/logs/access.log/download')
  })

  it('connects to live logs websocket and handles lifecycle events', () => {
    const received: LiveLogEntry[] = []
    const onOpen = vi.fn()
    const onError = vi.fn()
    const onClose = vi.fn()

    const disconnect = connectLiveLogs({ level: 'error', source: 'cerberus' }, (log) => received.push(log), onOpen, onError, onClose)

  const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    expect(socket.url).toContain('level=error')
    expect(socket.url).toContain('source=cerberus')

    socket.open()
    expect(onOpen).toHaveBeenCalled()

    socket.sendMessage(JSON.stringify({ level: 'info', timestamp: 'now', message: 'hello' }))
    expect(received).toHaveLength(1)

    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
    socket.sendMessage('not-json')
    expect(consoleError).toHaveBeenCalled()
    consoleError.mockRestore()

    const errorEvent = new Event('error')
    socket.triggerError(errorEvent)
    expect(onError).toHaveBeenCalledWith(errorEvent)

    socket.close()
    expect(onClose).toHaveBeenCalled()

    disconnect()
  })
})

describe('connectSecurityLogs', () => {
  it('connects to cerberus logs websocket endpoint', () => {
    const received: SecurityLogEntry[] = []
    const onOpen = vi.fn()

    connectSecurityLogs({}, (log) => received.push(log), onOpen)

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    expect(socket.url).toContain('/api/v1/cerberus/logs/ws')
  })

  it('passes source filter to websocket url', () => {
    connectSecurityLogs({ source: 'waf' }, () => {})

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    expect(socket.url).toContain('source=waf')
  })

  it('passes level filter to websocket url', () => {
    connectSecurityLogs({ level: 'error' }, () => {})

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    expect(socket.url).toContain('level=error')
  })

  it('passes ip filter to websocket url', () => {
    connectSecurityLogs({ ip: '192.168' }, () => {})

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    expect(socket.url).toContain('ip=192.168')
  })

  it('passes host filter to websocket url', () => {
    connectSecurityLogs({ host: 'example.com' }, () => {})

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    expect(socket.url).toContain('host=example.com')
  })

  it('passes blocked_only filter to websocket url', () => {
    connectSecurityLogs({ blocked_only: true }, () => {})

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    expect(socket.url).toContain('blocked_only=true')
  })

  it('receives and parses security log entries', () => {
    const received: SecurityLogEntry[] = []
    connectSecurityLogs({}, (log) => received.push(log))

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    socket.open()

    const securityLogEntry: SecurityLogEntry = {
      timestamp: '2025-12-12T10:30:00Z',
      level: 'info',
      logger: 'http.log.access',
      client_ip: '192.168.1.100',
      method: 'GET',
      uri: '/api/test',
      status: 200,
      duration: 0.05,
      size: 1024,
      user_agent: 'TestAgent/1.0',
      host: 'example.com',
      source: 'normal',
      blocked: false,
    }

    socket.sendMessage(JSON.stringify(securityLogEntry))

    expect(received).toHaveLength(1)
    expect(received[0].client_ip).toBe('192.168.1.100')
    expect(received[0].source).toBe('normal')
    expect(received[0].blocked).toBe(false)
  })

  it('receives blocked security log entries', () => {
    const received: SecurityLogEntry[] = []
    connectSecurityLogs({}, (log) => received.push(log))

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    socket.open()

    const blockedEntry: SecurityLogEntry = {
      timestamp: '2025-12-12T10:30:00Z',
      level: 'warn',
      logger: 'http.handlers.waf',
      client_ip: '10.0.0.1',
      method: 'POST',
      uri: '/admin',
      status: 403,
      duration: 0.001,
      size: 0,
      user_agent: 'Attack/1.0',
      host: 'example.com',
      source: 'waf',
      blocked: true,
      block_reason: 'SQL injection detected',
    }

    socket.sendMessage(JSON.stringify(blockedEntry))

    expect(received).toHaveLength(1)
    expect(received[0].blocked).toBe(true)
    expect(received[0].block_reason).toBe('SQL injection detected')
    expect(received[0].source).toBe('waf')
  })

  it('handles onOpen callback', () => {
    const onOpen = vi.fn()
    connectSecurityLogs({}, () => {}, onOpen)

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    socket.open()

    expect(onOpen).toHaveBeenCalled()
  })

  it('handles onError callback', () => {
    const onError = vi.fn()
    connectSecurityLogs({}, () => {}, undefined, onError)

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    const errorEvent = new Event('error')
    socket.triggerError(errorEvent)

    expect(onError).toHaveBeenCalledWith(errorEvent)
  })

  it('handles onClose callback', () => {
    const onClose = vi.fn()
    connectSecurityLogs({}, () => {}, undefined, undefined, onClose)

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    socket.close()

    expect(onClose).toHaveBeenCalled()
  })

  it('returns disconnect function that closes websocket', () => {
    const disconnect = connectSecurityLogs({}, () => {})

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    socket.open()

    expect(socket.readyState).toBe(MockWebSocket.OPEN)

    disconnect()

    expect(socket.readyState).toBe(MockWebSocket.CLOSED)
  })

  it('handles JSON parse errors gracefully', () => {
    const received: SecurityLogEntry[] = []
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})

    connectSecurityLogs({}, (log) => received.push(log))

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    socket.open()
    socket.sendMessage('invalid-json')

    expect(received).toHaveLength(0)
    expect(consoleError).toHaveBeenCalled()

    consoleError.mockRestore()
  })

  it('uses wss protocol when on https', () => {
    Object.defineProperty(window, 'location', {
      value: { protocol: 'https:', host: 'secure.example.com', href: '' },
      writable: true,
    })

    connectSecurityLogs({}, () => {})

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    expect(socket.url).toContain('wss://')
    expect(socket.url).toContain('secure.example.com')
  })

  it('combines multiple filters in websocket url', () => {
    connectSecurityLogs(
      {
        source: 'waf',
        level: 'warn',
        ip: '10.0.0',
        host: 'example.com',
        blocked_only: true,
      },
      () => {}
    )

    const socket = MockWebSocket.instances[MockWebSocket.instances.length - 1]!
    expect(socket.url).toContain('source=waf')
    expect(socket.url).toContain('level=warn')
    expect(socket.url).toContain('ip=10.0.0')
    expect(socket.url).toContain('host=example.com')
    expect(socket.url).toContain('blocked_only=true')
  })
})
