import { fileURLToPath } from 'node:url'
import { readFileSync } from 'node:fs'
import { compile } from '@tailwindcss/node'
import { describe, expect, it } from 'vitest'
import { TOKEN_COLORS, TOKEN_RADII, TOKEN_FONT_SIZES } from './tokens'

const VIEWPORT_BREAKPOINTS = {
  sm: '40rem',
  md: '48rem',
  lg: '64rem',
  xl: '80rem',
  '2xl': '96rem',
} as const

/** Tailwind v4 stock --container-* scale (NOT overridden by theme.css). */
const STOCK_CONTAINER_WIDTHS = {
  sm: '24rem',
  md: '28rem',
  '2xl': '42rem',
} as const

const base = readFileSync(new URL('./base.css', import.meta.url), 'utf8')
const css = readFileSync(new URL('./theme.css', import.meta.url), 'utf8')
const srcDir = fileURLToPath(new URL('.', import.meta.url))

describe('theme.css Tailwind v4 adapter', () => {
  it('is an @theme inline block (no bundled tailwindcss import)', () => {
    expect(css).toContain('@theme inline')
    // reject an actual @import directive (line-anchored) — NOT the substring
    // inside the doc comment, which references the consumer's own import order
    expect(/^\s*@import\s+["']tailwindcss["']/m.test(css)).toBe(false)
  })
  it('maps every token color to Tailwind --color-* → var(--mod-color-*)', () => {
    for (const c of TOKEN_COLORS) {
      expect(css).toContain(`--color-${c}: var(--mod-color-${c});`)
    }
  })
  it('base.css defines a reference value for every token color (no orphan mapping)', () => {
    for (const c of TOKEN_COLORS) {
      expect(base).toMatch(new RegExp(`--mod-color-${c}:\\s*#`))
    }
  })
  it('maps every radius to --radius-* → var(--mod-radius-*)', () => {
    for (const r of TOKEN_RADII) {
      expect(css).toContain(`--radius-${r}: var(--mod-radius-${r});`)
    }
  })
  it('maps every font-size to --text-* → var(--mod-font-size-*)', () => {
    for (const s of TOKEN_FONT_SIZES) {
      expect(css).toContain(`--text-${s}: var(--mod-font-size-${s});`)
    }
  })
  it('declares viewport breakpoints in a standard @theme block (Tailwind namespace)', () => {
    expect(css).toMatch(/@theme\s*\{[\s\S]*--breakpoint-sm:\s*40rem/)
    for (const [key, value] of Object.entries(VIEWPORT_BREAKPOINTS)) {
      expect(css).toContain(`--breakpoint-${key}: ${value};`)
    }
  })
  it('does not override --container-* (shared namespace — backs max-w-* and @md:)', () => {
    expect(css).not.toMatch(/--container-[^:]+:\s*[^;]+;/)
  })
  it('does not map breakpoints through the runtime --mod-* namespace', () => {
    expect(css).not.toMatch(/--breakpoint-[^:]+:\s*var\(--mod-/)
    expect(css).not.toMatch(/--container-[^:]+:\s*var\(--mod-/)
  })
})

// Regression gate: substring assertions above can't see a malformed @theme block
// (e.g. a comment whose `*/` closes early, leaking text into @theme). Only the real
// Tailwind v4 engine — the same `@tailwindcss/node` path the consumer's bundler runs —
// rejects that. Compile base + theme as a consumer would; a defect throws here.
describe('theme.css compiles through the real Tailwind v4 engine', () => {
  async function compileTheme(utilities: string[]) {
    const input = `@import "tailwindcss";\n${base}\n${css}`
    const compiler = await compile(input, { base: srcDir, onDependency: () => {} })
    return compiler.build(utilities)
  }

  it('compiles base + theme and emits token-backed utilities (no malformed @theme)', async () => {
    const out = await compileTheme([
      'bg-surface',
      'text-fg',
      'rounded-md',
      'text-3xl',
      'md:flex',
      '@md:flex',
      '@container',
    ])
    // utilities emit and resolve to the --mod-* contract → @theme mapping is intact
    expect(out).toContain('.bg-surface')
    expect(out).toContain('.text-3xl')
    expect(out).toContain('--mod-color-surface')
    // responsive variants emit from the breakpoint/container @theme contract
    expect(out).toContain('.md\\:flex')
    expect(out).toMatch(/@media \(width >= 48rem\)/)
    expect(out).toContain('.\\@md\\:flex')
    expect(out).toMatch(/@container \(width >= 28rem\)/)
    expect(out).toContain('.\\@container')
  })

  it('keeps max-w-* on Tailwind stock --container-* scale (regression guard)', async () => {
    const out = await compileTheme(['max-w-sm', 'max-w-md', 'max-w-2xl'])
    expect(out).toMatch(/\.max-w-sm[\s\S]*max-width:\s*var\(--container-sm\)/)
    expect(out).toMatch(/\.max-w-md[\s\S]*max-width:\s*var\(--container-md\)/)
    expect(out).toMatch(/\.max-w-2xl[\s\S]*max-width:\s*var\(--container-2xl\)/)
    expect(out).toContain(`--container-sm: ${STOCK_CONTAINER_WIDTHS.sm}`)
    expect(out).toContain(`--container-md: ${STOCK_CONTAINER_WIDTHS.md}`)
    expect(out).toContain(`--container-2xl: ${STOCK_CONTAINER_WIDTHS['2xl']}`)
  })

  it('does not clobber non-breakpoint utilities (breakpoint sweep)', async () => {
    const themed = await compileTheme(['max-w-md', 'lg:grid-cols-3'])
    const stockInput = `@import "tailwindcss";\n${base}`
    const stockCompiler = await compile(stockInput, { base: srcDir, onDependency: () => {} })
    const stock = stockCompiler.build(['max-w-md', 'lg:grid-cols-3'])

    // max-w-* must stay on stock --container-md (28rem), not viewport md (48rem)
    expect(themed).toContain('--container-md: 28rem')
    expect(stock).toContain('--container-md: 28rem')

    // v4 dropped max-w-screen-* — no screen-width utility family to rescale
    expect(themed).not.toMatch(/max-w-screen/)
    expect(stock).not.toMatch(/max-w-screen/)

    // only viewport breakpoints change: lg: uses our 64rem, stock Tailwind defaults to 64rem too
    // (same value here) — assert the themed output still emits lg: and uses breakpoint-lg
    expect(themed).toContain('.lg\\:grid-cols-3')
    expect(themed).toMatch(/@media \(width >= 64rem\)/)
  })
})
