import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { PDFRenderer, renderTemplate } from '../renderer'
import type { PDFTemplateData } from '../types'

// Mock Playwright
vi.mock('playwright', () => {
  const mockPage = {
    setContent: vi.fn().mockResolvedValue(undefined),
    pdf: vi.fn().mockResolvedValue(Buffer.from('mock pdf content')),
    close: vi.fn().mockResolvedValue(undefined),
  }

  const mockBrowser = {
    newPage: vi.fn().mockResolvedValue(mockPage),
    close: vi.fn().mockResolvedValue(undefined),
  }

  return {
    chromium: {
      launch: vi.fn().mockResolvedValue(mockBrowser),
    },
  }
})

describe('PDFRenderer', () => {
  let renderer: PDFRenderer

  beforeEach(() => {
    renderer = new PDFRenderer()
    vi.clearAllMocks()
  })

  afterEach(async () => {
    await renderer.close()
  })

  describe('initialize', () => {
    it('should launch chromium browser', async () => {
      const { chromium } = await import('playwright')

      await renderer.initialize()

      expect(chromium.launch).toHaveBeenCalledWith({ headless: true })
    })

    it('should not relaunch browser on second initialize', async () => {
      const { chromium } = await import('playwright')

      await renderer.initialize()
      vi.clearAllMocks()
      await renderer.initialize()

      expect(chromium.launch).not.toHaveBeenCalled()
    })
  })

  describe('renderPDF', () => {
    it('should render HTML to PDF buffer', async () => {
      const html = '<html><body>Test PDF</body></html>'

      const result = await renderer.renderPDF(html)

      expect(result).toBeInstanceOf(Buffer)
      expect(result.toString()).toContain('mock pdf content')
    })

    it('should set page content with correct options', async () => {
      const { chromium } = await import('playwright')
      const html = '<html><body>Test</body></html>'

      await renderer.renderPDF(html)

      const mockBrowser = await (chromium.launch as any)()
      const mockPage = await mockBrowser.newPage()

      expect(mockPage.setContent).toHaveBeenCalledWith(html, { waitUntil: 'networkidle' })
    })

    it('should generate PDF with A4 format', async () => {
      const { chromium } = await import('playwright')
      const html = '<html><body>Test</body></html>'

      await renderer.renderPDF(html)

      const mockBrowser = await (chromium.launch as any)()
      const mockPage = await mockBrowser.newPage()

      expect(mockPage.pdf).toHaveBeenCalledWith({
        format: 'A4',
        margin: { top: '0', right: '0', bottom: '0', left: '0' },
        timeout: 30000,
      })
    })

    it('should respect custom timeout', async () => {
      const { chromium } = await import('playwright')
      const html = '<html><body>Test</body></html>'
      const customTimeout = 60000

      await renderer.renderPDF(html, customTimeout)

      const mockBrowser = await (chromium.launch as any)()
      const mockPage = await mockBrowser.newPage()

      expect(mockPage.pdf).toHaveBeenCalledWith(
        expect.objectContaining({
          timeout: customTimeout,
        })
      )
    })

    it('should close page after rendering', async () => {
      const { chromium } = await import('playwright')
      const html = '<html><body>Test</body></html>'

      await renderer.renderPDF(html)

      const mockBrowser = await (chromium.launch as any)()
      const mockPage = await mockBrowser.newPage()

      expect(mockPage.close).toHaveBeenCalled()
    })

    it('should handle rendering errors', async () => {
      const { chromium } = await import('playwright')
      const html = '<html><body>Test</body></html>'

      const mockBrowser = await (chromium.launch as any)()
      const mockPage = await mockBrowser.newPage()
      ;(mockPage.pdf as any).mockRejectedValueOnce(new Error('PDF generation failed'))

      await expect(renderer.renderPDF(html)).rejects.toThrow()
    })
  })

  describe('browser health check', () => {
    it('should initialize browser if not initialized', async () => {
      const { chromium } = await import('playwright')

      const html = '<html><body>Test</body></html>'
      await renderer.renderPDF(html)

      expect(chromium.launch).toHaveBeenCalled()
    })

    it('should restart browser if lifetime exceeded', async () => {
      const { chromium } = await import('playwright')
      vi.useFakeTimers()

      try {
        await renderer.initialize()
        vi.clearAllMocks()

        // Advance time beyond MAX_BROWSER_LIFETIME (3600000ms)
        vi.advanceTimersByTime(3600001)

        const html = '<html><body>Test</body></html>'
        await renderer.renderPDF(html)

        // Should have launched a new browser
        expect(chromium.launch).toHaveBeenCalled()
      } finally {
        vi.useRealTimers()
      }
    })
  })

  describe('close', () => {
    it('should close browser', async () => {
      const { chromium } = await import('playwright')

      await renderer.initialize()
      await renderer.close()

      const mockBrowser = await (chromium.launch as any)()
      expect(mockBrowser.close).toHaveBeenCalled()
    })

    it('should set browser to null after closing', async () => {
      await renderer.initialize()
      await renderer.close()

      // Verify browser is null by checking that next initialize calls launch
      const { chromium } = await import('playwright')
      vi.clearAllMocks()

      await renderer.initialize()
      expect(chromium.launch).toHaveBeenCalled()
    })
  })
})

describe('renderTemplate', () => {
  const testData: PDFTemplateData = {
    asn: 13335,
    network_name: 'Cloudflare Network',
    generated_at: '2026-04-29T10:00:00Z',
    health_score: {
      overall: 78,
      aspa: 65,
      rpki: 85,
      bgp_stability: 80,
      peering_health: 75,
    },
    aspa: {
      adoption_status: 'in_progress',
      provider_verification: 45,
      readiness_score: 60,
    },
    peering: {
      ixp_connections: 12,
      peer_count: 42,
      open_peers: 28,
      route_exports: 1250,
    },
    threats: {
      recent_hijacks: 0,
      anomalies_detected: 2,
      rpki_invalids: 3,
      moas_events: 1,
    },
    recommendations: [
      'Implement RPKI ROV',
      'Increase ASPA adoption',
      'Monitor anomalies',
    ],
    data_sources: [
      'RIPE RIS Route Collectors',
      'RouteViews BGP Archive',
    ],
  }

  describe('simple variable replacement', () => {
    it('should replace simple variables', () => {
      const template = 'ASN: {{asn}}'
      const result = renderTemplate(template, testData)

      expect(result).toBe('ASN: 13335')
    })

    it('should replace multiple variables', () => {
      const template = 'Network: {{network_name}} (ASN {{asn}})'
      const result = renderTemplate(template, testData)

      expect(result).toBe('Network: Cloudflare Network (ASN 13335)')
    })

    it('should handle string conversion for non-string types', () => {
      const template = 'Score: {{health_score.overall}}'
      const result = renderTemplate(template, testData)

      expect(result).toBe('Score: 78')
    })
  })

  describe('nested object replacement', () => {
    it('should replace nested variables', () => {
      const template = 'Health Score: {{health_score.overall}}/100'
      const result = renderTemplate(template, testData)

      expect(result).toBe('Health Score: 78/100')
    })

    it('should replace multiple nested variables', () => {
      const template = 'ASPA: {{aspa.adoption_status}}, Readiness: {{aspa.readiness_score}}%'
      const result = renderTemplate(template, testData)

      expect(result).toBe('ASPA: in_progress, Readiness: 60%')
    })

    it('should handle multiple different nested objects', () => {
      const template = 'Peers: {{peering.peer_count}}, Threats: {{threats.anomalies_detected}}'
      const result = renderTemplate(template, testData)

      expect(result).toBe('Peers: 42, Threats: 2')
    })
  })

  describe('array iteration with #each', () => {
    it('should render array items', () => {
      const template =
        'Recommendations:\n{{#each recommendations}}<li>{{this}}</li>\n{{/each}}'
      const result = renderTemplate(template, testData)

      expect(result).toContain('<li>Implement RPKI ROV</li>')
      expect(result).toContain('<li>Increase ASPA adoption</li>')
      expect(result).toContain('<li>Monitor anomalies</li>')
    })

    it('should handle empty arrays', () => {
      const template = 'Sources: {{#each data_sources}}<li>{{this}}</li>{{/each}}'
      const dataWithEmpty = { ...testData, data_sources: [] }

      const result = renderTemplate(template, dataWithEmpty)

      expect(result).toBe('Sources: ')
    })

    it('should handle nested HTML in array iteration', () => {
      const template =
        '{{#each recommendations}}<div class="rec">{{this}}</div>{{/each}}'
      const result = renderTemplate(template, testData)

      expect(result).toContain('<div class="rec">Implement RPKI ROV</div>')
    })

    it('should replace placeholders in array items', () => {
      const template = '{{#each data_sources}}<source>{{this}}</source>{{/each}}'
      const result = renderTemplate(template, testData)

      expect(result).toContain('<source>RIPE RIS Route Collectors</source>')
      expect(result).toContain('<source>RouteViews BGP Archive</source>')
    })
  })

  describe('complex template scenarios', () => {
    it('should handle full template with mixed replacements', () => {
      const template = `
        <h1>{{network_name}}</h1>
        <p>ASN: {{asn}}</p>
        <p>Health Score: {{health_score.overall}}</p>
        <ul>
          {{#each recommendations}}<li>{{this}}</li>{{/each}}
        </ul>
      `

      const result = renderTemplate(template, testData)

      expect(result).toContain('Cloudflare Network')
      expect(result).toContain('ASN: 13335')
      expect(result).toContain('Health Score: 78')
      expect(result).toContain('<li>Implement RPKI ROV</li>')
    })

    it('should handle case-sensitive variable names', () => {
      const template = '{{asn}} {{ASN}}'
      const result = renderTemplate(template, testData)

      expect(result).toContain('13335')
      expect(result).toContain('{{ASN}}') // Not replaced since case doesn't match
    })

    it('should preserve formatting around variables', () => {
      const template = 'The score is {{health_score.overall}} out of 100'
      const result = renderTemplate(template, testData)

      expect(result).toBe('The score is 78 out of 100')
    })
  })

  describe('error handling and edge cases', () => {
    it('should handle template with no variables', () => {
      const template = '<html><body>Static content</body></html>'
      const result = renderTemplate(template, testData)

      expect(result).toBe(template)
    })

    it('should not fail on undefined nested properties', () => {
      const template = '{{nonexistent.property}}'
      const result = renderTemplate(template, testData)

      // Should not throw, just leave as-is or convert undefined to string
      expect(typeof result).toBe('string')
    })

    it('should handle whitespace in variable names', () => {
      const template = 'Value: {{ asn }}' // Space around variable name
      const result = renderTemplate(template, testData)

      // Should not match due to spaces
      expect(result).toContain('{{ asn }}')
    })

    it('should handle special regex characters in values', () => {
      const dataWithSpecialChars = {
        ...testData,
        network_name: 'Test [Network] (Special)',
      }

      const template = 'Network: {{network_name}}'
      const result = renderTemplate(template, dataWithSpecialChars)

      expect(result).toContain('Test [Network] (Special)')
    })
  })
})
