import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { renderToString } from 'react-dom/server'
import type { ConsentStorage } from './useConsent.js'
import { ConsentBanner } from './ConsentBanner.js'

function memStorage(initial: string | null = null): ConsentStorage & { value: string | null } {
  const box = { value: initial }
  return {
    value: box.value,
    load() {
      return box.value
    },
    save(v: string) {
      box.value = v
      ;(this as { value: string | null }).value = v
    },
  }
}

describe('ConsentBanner', () => {
  it('renders when no consent is stored', () => {
    render(<ConsentBanner version="1" storage={memStorage(null)} />)
    expect(screen.getByRole('region', { name: /cookie/i })).toBeTruthy()
    expect(screen.getByRole('button', { name: /accept all/i })).toBeTruthy()
    expect(screen.getByRole('button', { name: /reject all/i })).toBeTruthy()
  })

  it('does not render once a version-matched consent is stored', () => {
    const stored = JSON.stringify({
      categories: { necessary: true, analytics: false, marketing: false, preferences: false },
      recordedAt: '2026-06-20T00:00:00.000Z',
      version: '1',
    })
    const { container } = render(<ConsentBanner version="1" storage={memStorage(stored)} />)
    expect(container.querySelector('[role="region"]')).toBeNull()
  })

  it('renders (re-consent) when open=true even with stored consent', () => {
    const stored = JSON.stringify({
      categories: { necessary: true, analytics: true, marketing: true, preferences: true },
      recordedAt: '2026-06-20T00:00:00.000Z',
      version: '1',
    })
    render(<ConsentBanner version="1" storage={memStorage(stored)} open />)
    expect(screen.getByRole('region', { name: /cookie/i })).toBeTruthy()
  })

  it('the necessary checkbox is checked and disabled (always-on, not editable)', () => {
    render(<ConsentBanner version="1" storage={memStorage(null)} />)
    const necessary = screen.getByRole('checkbox', { name: /strictly necessary/i }) as HTMLInputElement
    expect(necessary.checked).toBe(true)
    expect(necessary.disabled).toBe(true)
  })

  it('non-necessary boxes start unchecked (privacy-by-default)', () => {
    render(<ConsentBanner version="1" storage={memStorage(null)} />)
    const analytics = screen.getByRole('checkbox', { name: /analytics/i }) as HTMLInputElement
    expect(analytics.checked).toBe(false)
  })

  it('Accept all persists all-on, fires onChange, and hides the banner', () => {
    const storage = memStorage(null)
    const onChange = vi.fn()
    render(<ConsentBanner version="1" storage={storage} onChange={onChange} />)
    fireEvent.click(screen.getByRole('button', { name: /accept all/i }))
    expect(onChange).toHaveBeenCalledTimes(1)
    expect(onChange.mock.calls[0]![0].categories.marketing).toBe(true)
    expect(JSON.parse(storage.value as string).categories.analytics).toBe(true)
    // banner hides after persisting
    expect(screen.queryByRole('region', { name: /cookie/i })).toBeNull()
  })

  it('Accept all persists even when onChange is omitted (no short-circuit)', () => {
    // onChange is an optional prop; a host may omit it. Persistence MUST NOT depend on it
    // (`onChange?.(acceptAll())` would never run acceptAll when onChange is nullish). Spec §5.3.
    const storage = memStorage(null)
    render(<ConsentBanner version="1" storage={storage} />)
    fireEvent.click(screen.getByRole('button', { name: /accept all/i }))
    expect(storage.value).not.toBeNull()
    expect(JSON.parse(storage.value as string).categories.marketing).toBe(true)
  })

  it('Reject all persists even when onChange is omitted (no short-circuit)', () => {
    const storage = memStorage(null)
    render(<ConsentBanner version="1" storage={storage} />)
    fireEvent.click(screen.getByRole('button', { name: /reject all/i }))
    expect(storage.value).not.toBeNull()
    expect(JSON.parse(storage.value as string).categories.analytics).toBe(false)
  })

  it('Reject all persists only necessary on', () => {
    const storage = memStorage(null)
    const onChange = vi.fn()
    render(<ConsentBanner version="1" storage={storage} onChange={onChange} />)
    fireEvent.click(screen.getByRole('button', { name: /reject all/i }))
    expect(onChange.mock.calls[0]![0].categories).toEqual({
      necessary: true,
      analytics: false,
      marketing: false,
      preferences: false,
    })
  })

  it('Save preferences persists the current checkbox selection', () => {
    const storage = memStorage(null)
    render(<ConsentBanner version="1" storage={storage} />)
    fireEvent.click(screen.getByRole('checkbox', { name: /analytics/i }))
    fireEvent.click(screen.getByRole('button', { name: /save preferences/i }))
    const saved = JSON.parse(storage.value as string)
    expect(saved.categories.analytics).toBe(true)
    expect(saved.categories.marketing).toBe(false)
  })

  it('renders a policy link when policyHref is set', () => {
    render(<ConsentBanner version="1" storage={memStorage(null)} policyHref="/privacy" />)
    const link = screen.getByRole('link', { name: /privacy policy/i }) as HTMLAnchorElement
    expect(link.getAttribute('href')).toBe('/privacy')
  })

  it('keeps an absolute http(s) policyHref', () => {
    render(
      <ConsentBanner
        version="1"
        storage={memStorage(null)}
        policyHref="https://example.com/privacy"
      />,
    )
    const link = screen.getByRole('link', { name: /privacy policy/i }) as HTMLAnchorElement
    expect(link.getAttribute('href')).toBe('https://example.com/privacy')
  })

  it('drops a javascript: policyHref (no href-execution sink) — link is not rendered', () => {
    // An adopter may bind policyHref from a CMS field; React does not strip dangerous schemes.
    // A javascript:/data:/vbscript: scheme must be rejected so no executable href reaches the DOM.
    render(
      <ConsentBanner
        version="1"
        storage={memStorage(null)}
        // eslint-disable-next-line no-script-url
        policyHref="javascript:alert(document.cookie)"
      />,
    )
    expect(screen.queryByRole('link', { name: /privacy policy/i })).toBeNull()
  })

  it('drops a javascript: scheme even with embedded control/whitespace chars', () => {
    render(
      <ConsentBanner
        version="1"
        storage={memStorage(null)}
        policyHref={'java\tscript:alert(1)'}
      />,
    )
    expect(screen.queryByRole('link', { name: /privacy policy/i })).toBeNull()
  })

  it('only shows the categories passed in `categories`', () => {
    render(<ConsentBanner version="1" storage={memStorage(null)} categories={['analytics']} />)
    expect(screen.queryByRole('checkbox', { name: /marketing/i })).toBeNull()
    expect(screen.getByRole('checkbox', { name: /analytics/i })).toBeTruthy()
  })

  it('renders nothing during SSR (no storage access in render; ready=false → null), even with stored consent', () => {
    // renderToString runs no effects → ready stays false → show=false → null output. This proves the
    // banner is SSR-stable: server render and first client render both produce null (no hydration mismatch).
    const stored = JSON.stringify({
      categories: { necessary: true, analytics: false, marketing: false, preferences: false },
      recordedAt: '2026-06-20T00:00:00.000Z',
      version: '1',
    })
    expect(renderToString(<ConsentBanner version="1" storage={memStorage(stored)} />)).toBe('')
  })
})
