import { describe, it, expect, vi, beforeEach } from 'vitest'

const mockRun = vi.fn((): any => ({ lastInsertRowid: 1, changes: 1 }))
const mockGet = vi.fn((): any => ({
  auth_failures: 1,
  injection_attempts: 0,
  rate_limit_hits: 0,
  secret_exposures: 0,
  successful_tasks: 5,
  failed_tasks: 0,
  trust_score: 0.95,
}))
const mockPrepare = vi.fn(() => ({ run: mockRun, get: mockGet, all: vi.fn(() => []) }))

vi.mock('@/lib/db', () => ({
  getDatabase: () => ({ prepare: mockPrepare }),
}))

vi.mock('@/lib/event-bus', () => ({
  eventBus: { broadcast: vi.fn() },
}))

vi.mock('@/lib/logger', () => ({
  logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
}))

import { logSecurityEvent, updateAgentTrustScore, getSecurityPosture } from '@/lib/security-events'

describe('logSecurityEvent', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockRun.mockReturnValue({ lastInsertRowid: 42, changes: 1 })
  })

  it('inserts an event into the database', () => {
    const id = logSecurityEvent({
      event_type: 'auth_failure',
      severity: 'warning',
      source: 'auth',
      detail: 'test detail',
    })

    expect(mockPrepare).toHaveBeenCalled()
    expect(mockRun).toHaveBeenCalledWith(
      'auth_failure', 'warning', 'auth', null, 'test detail', null, 1, 1
    )
    expect(id).toBe(42)
  })

  it('defaults severity to info when not provided', () => {
    logSecurityEvent({ event_type: 'test_event' })
    expect(mockRun).toHaveBeenCalledWith(
      'test_event', 'info', null, null, null, null, 1, 1
    )
  })

  it('uses provided workspace_id and tenant_id', () => {
    logSecurityEvent({
      event_type: 'test_event',
      severity: 'critical',
      workspace_id: 5,
      tenant_id: 3,
    })
    expect(mockRun).toHaveBeenCalledWith(
      'test_event', 'critical', null, null, null, null, 5, 3
    )
  })

  it('broadcasts via event bus', async () => {
    const { eventBus } = await import('@/lib/event-bus')
    logSecurityEvent({ event_type: 'injection_attempt', severity: 'critical' })
    expect(eventBus.broadcast).toHaveBeenCalledWith(
      'security.event',
      expect.objectContaining({ event_type: 'injection_attempt', severity: 'critical' })
    )
  })
})

describe('updateAgentTrustScore', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockGet.mockReturnValue({
      auth_failures: 1,
      injection_attempts: 0,
      rate_limit_hits: 0,
      secret_exposures: 0,
      successful_tasks: 5,
      failed_tasks: 0,
      trust_score: 0.95,
    })
  })

  it('creates a row if one does not exist (INSERT OR IGNORE)', () => {
    updateAgentTrustScore('test-agent', 'auth.failure', 1)
    // First call: INSERT OR IGNORE, second: UPDATE counter, third: SELECT, fourth: UPDATE score
    expect(mockPrepare).toHaveBeenCalled()
    expect(mockRun).toHaveBeenCalled()
  })

  it('recalculates trust score clamped between 0 and 1', () => {
    mockGet.mockReturnValue({
      auth_failures: 20,
      injection_attempts: 10,
      rate_limit_hits: 5,
      secret_exposures: 3,
      successful_tasks: 0,
      failed_tasks: 0,
      trust_score: 0,
    })

    updateAgentTrustScore('bad-agent', 'injection.attempt', 1)
    // Score would go negative, should be clamped to 0
    const calls = mockRun.mock.calls as any[][]
    const lastCall = calls[calls.length - 1]
    if (typeof lastCall[0] === 'number') {
      expect(lastCall[0]).toBeGreaterThanOrEqual(0)
      expect(lastCall[0]).toBeLessThanOrEqual(1)
    }
  })
})

describe('getSecurityPosture', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('returns expected posture shape', () => {
    mockGet
      .mockReturnValueOnce({ total: 10, critical: 2, warning: 5 })
      .mockReturnValueOnce({ count: 3 })
      .mockReturnValueOnce({ avg_trust: 0.85 })

    const posture = getSecurityPosture(1)
    expect(posture).toHaveProperty('score')
    expect(posture).toHaveProperty('totalEvents')
    expect(posture).toHaveProperty('criticalEvents')
    expect(posture).toHaveProperty('warningEvents')
    expect(posture).toHaveProperty('avgTrustScore')
    expect(posture).toHaveProperty('recentIncidents')
    expect(typeof posture.score).toBe('number')
    expect(posture.score).toBeGreaterThanOrEqual(0)
    expect(posture.score).toBeLessThanOrEqual(100)
  })

  it('deducts points for critical and warning events', () => {
    mockGet
      .mockReturnValueOnce({ total: 5, critical: 5, warning: 0 })
      .mockReturnValueOnce({ count: 5 })
      .mockReturnValueOnce({ avg_trust: 1.0 })

    const posture = getSecurityPosture(1)
    expect(posture.score).toBeLessThan(100)
  })

  it('returns score of 100 with no events', () => {
    mockGet
      .mockReturnValueOnce({ total: 0, critical: 0, warning: 0 })
      .mockReturnValueOnce({ count: 0 })
      .mockReturnValueOnce({ avg_trust: 1.0 })

    const posture = getSecurityPosture(1)
    expect(posture.score).toBe(100)
  })
})

describe('injection guard new rules', () => {
  let scanForInjection: typeof import('@/lib/injection-guard').scanForInjection

  beforeEach(async () => {
    const mod = await import('@/lib/injection-guard')
    scanForInjection = mod.scanForInjection
  })

  it('detects SSRF targeting metadata endpoint', () => {
    const report = scanForInjection('curl http://169.254.169.254/latest/meta-data/', { context: 'shell' })
    expect(report.safe).toBe(false)
    expect(report.matches.some(m => m.rule === 'cmd-ssrf')).toBe(true)
  })

  it('detects SSRF targeting localhost', () => {
    const report = scanForInjection('wget http://localhost:8080/admin', { context: 'shell' })
    expect(report.safe).toBe(false)
    expect(report.matches.some(m => m.rule === 'cmd-ssrf')).toBe(true)
  })

  it('detects template injection (Jinja2)', () => {
    const report = scanForInjection('{{config.__class__.__init__.__globals__}}', { context: 'prompt' })
    expect(report.safe).toBe(false)
    expect(report.matches.some(m => m.rule === 'cmd-template-injection')).toBe(true)
  })

  it('detects SQL injection (UNION SELECT)', () => {
    const report = scanForInjection("' UNION SELECT * FROM users --", { context: 'shell' })
    expect(report.safe).toBe(false)
    expect(report.matches.some(m => m.rule === 'cmd-sql-injection')).toBe(true)
  })

  it('detects SQL injection (OR 1=1)', () => {
    const report = scanForInjection("' OR 1=1 --", { context: 'shell' })
    expect(report.safe).toBe(false)
    expect(report.matches.some(m => m.rule === 'cmd-sql-injection')).toBe(true)
  })

  it('does not false-positive on normal SQL mentions', () => {
    const report = scanForInjection('SELECT name FROM products WHERE id = 5', { context: 'shell' })
    // This should not trigger because it lacks injection markers
    expect(report.matches.filter(m => m.rule === 'cmd-sql-injection')).toHaveLength(0)
  })
})
