import { parseCandidateStrings, IO, Parsing } from '@tailwindcss/oxide'
import { defaultExtractor as createDefaultExtractor } from '../src/lib/defaultExtractor'

let html = String.raw

let defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })

function regexParser(str) {
  return defaultExtractor(str)
}

function oxideParser(str) {
  return parseCandidateStrings(
    [{ content: str, extension: 'html' }],
    IO.Sequential | Parsing.Parallel
  )
}

function templateTable(classes) {
  classes = classes.concat(
    // Variants
    classes.flatMap((c) => {
      return [
        // Simple variant
        `hover:${c}`,

        // Combined variant
        `focus:hover:${c}`,

        // Variant with dashes
        `group-hover:group-focus:${c}`,

        // With special characters
        `<sm:${c}`,
        `md>:${c}`,

        // With arbitrary values
        `min-[300px]:${c}`,

        // With arbitrary values
        `[@media(hover:hover)]:${c}`,

        // With parent selector
        `[.foo_&]:${c}`,
      ]
    })
  )

  let classString = classes.join(' ')
  let singleQuoteArraySyntaxWithSpace = `'${classes.join("', '")}'`
  let singleQuoteArraySyntaxWithoutSpace = `'${classes.join("','")}'`

  return [
    ['Plain', classString],
    ['HTML', html`<div class="${classString}"></div>`],

    ['JavaScript variable', `let foo = "${classString}"`],
    ['JavaScript expression', `document.body.classList.add(['${classString}'].join(' '))`],
    ['JavaScript object key', `let foo = {'${classString}': true}`],

    ['JSX basic', html`<div className="${classString}"></div>`],
    ['JSX with JavaScript expression', html`<div className={"${classString}"}></div>`],

    ['Vue basic', html`<div :class="${classString}"></div>`],
    [
      'Vue array (single quote, with space)',
      html`<div :class="[${singleQuoteArraySyntaxWithSpace}]"></div>`,
    ],
    [
      'Vue array (single quote, without space)',
      html`<div :class="[${singleQuoteArraySyntaxWithoutSpace}]"></div>`,
    ],
    ['Vue object (single quote)', html`<div :class="{'${classString}': true}"></div>`],

    ['Markdown code fences', `<!-- This should work \`${classString}\` -->`],
  ].map(([name, template]) => [name, template, classes])
}

describe.each([
  ['Regex', regexParser],
  ['Oxide', oxideParser],
])('%s parser', (_, parse) => {
  describe('basic utility classes', () => {
    let classes = [
      // One word classes
      'underline',

      // With dashes
      'text-center',
      'pointer-events-none',

      // With numbers
      'px-4',

      // With special characters
      'px-1.5',
      'translate-x-1/2',
      'from-50%',

      // With negative signs
      '-translate-x-full',

      // With halves and negative signs
      '-translate-x-1/2',
    ]

    test.each(templateTable(classes))('%# — %s', (_, template, classes) => {
      let extractions = parse(template)

      for (let c of classes) {
        expect(extractions).toContain(c)
      }
    })
  })

  describe('utility classes with arbitrary values', () => {
    let classes = [
      // With simple number
      'px-[0]',
      'px-[0.5]',

      // With number and unit
      'px-[123px]',
      'px-[123.45px]',

      // With special symbols
      'bg-[#bada55]',
      //   ^
      'bg-[color:#bada55]',
      //        ^^
      'content-[>]',
      //        ^
      'content-[<]',
      //        ^

      // With functions and math expressions
      'px-[calc(100%-1rem)]',
      'px-[theme(spacing.1)]',
      'px-[theme(spacing[1.5])]',

      // With spaces (replaced by `_`)
      'bg-[rgb(255_0_0)]',

      // Examples with combinations
      'w-[calc(100%_-_theme("spacing[1.5]))"]',
      'fill-[oklab(59.69%_0.1007_0.1191_/_0.5)]/[33.7%]',
      'fill-[color:oklab(59.69%_0.1007_0.1191_/_0.5)]/[33.7%]',
      'shadow-[inset_0_-3em_3em_rgba(0,_0,_0,_0.1),_0_0_0_2px_rgb(255,_255,_255),_0.3em_0.3em_1em_rgba(0,_0,_0,_0.3)]',
    ]

    test.each(templateTable(classes))('%# — %s', (_, template, classes) => {
      let extractions = parse(template)

      for (let c of classes) {
        // TODO: This is a bug in the RegEx parser.
        if (!extractions.includes(c) && parse === regexParser) {
          continue
        }

        expect(extractions).toContain(c)
      }
    })
  })

  describe('utility classes with modifiers', () => {
    let classes = [
      // With simple modifiers
      'bg-red-500/50',

      // With arbitrary modifiers
      'bg-red-500/[0.5]',
      'bg-red-500/[50%]',
      'bg-red-500/[var(--opacity)]',

      // With spces (replaced by `_`)
      'bg-red-500/[var(--opacity,_50%)]',
    ]

    test.each(templateTable(classes))('%# — %s', (_, template, classes) => {
      let extractions = parse(template)

      for (let c of classes) {
        expect(extractions).toContain(c)
      }
    })
  })

  describe('utility classes with important modifier', () => {
    let classes = ['!bg-red-500', '!bg-[#bada55]', '![display:flex]', '!-translate-x-1/2']

    test.each(templateTable(classes))('%# — %s', (_, template, classes) => {
      let extractions = parse(template)

      for (let c of classes) {
        // TODO: This is a bug in the RegEx parser.
        if (!extractions.includes(c) && parse === regexParser) {
          continue
        }

        expect(extractions).toContain(c)
      }
    })
  })

  describe('arbitrary properties', () => {
    let classes = [
      // With simple arbitrary property
      '[display:flex]',
    ]

    test.each(templateTable(classes))('%# — %s', (_, template, classes) => {
      let extractions = parse(template)

      for (let c of classes) {
        expect(extractions).toContain(c)
      }
    })
  })

  describe('arbitrary values with quotes', () => {
    test('arbitrary values with single quotes', async () => {
      let extractions = parse(`
      <div class="content-['hello_world'] hover:content-['hello_world_2'] hover:focus:content-['hello_world_3']"></div>
    `)

      expect(extractions).toContain(`content-['hello_world']`)
      expect(extractions).toContain(`hover:content-['hello_world_2']`)
      expect(extractions).toContain(`hover:focus:content-['hello_world_3']`)
    })

    test('arbitrary values with double quotes', async () => {
      let extractions = parse(`
      <div class='content-["hello_world"] hover:content-["hello_world_2"] hover:focus:content-["hello_world_3"]'></div>
    `)

      expect(extractions).toContain(`content-["hello_world"]`)
      expect(extractions).toContain(`hover:content-["hello_world_2"]`)
      expect(extractions).toContain(`hover:focus:content-["hello_world_3"]`)
    })

    test('arbitrary values with some single quoted values', async () => {
      let extractions = parse(`
      <div class="font-['Open_Sans',_system-ui,_sans-serif] hover:font-['Proxima_Nova',_system-ui,_sans-serif] hover:focus:font-['Inter_var',_system-ui,_sans-serif]"></div>
    `)

      expect(extractions).toContain(`font-['Open_Sans',_system-ui,_sans-serif]`)
      expect(extractions).toContain(`hover:font-['Proxima_Nova',_system-ui,_sans-serif]`)
      expect(extractions).toContain(`hover:focus:font-['Inter_var',_system-ui,_sans-serif]`)
    })

    test('arbitrary values with some double quoted values', async () => {
      let extractions = parse(`
      <div class='font-["Open_Sans",_system-ui,_sans-serif] hover:font-["Proxima_Nova",_system-ui,_sans-serif] hover:focus:font-["Inter_var",_system-ui,_sans-serif]'></div>
    `)

      expect(extractions).toContain(`font-["Open_Sans",_system-ui,_sans-serif]`)
      expect(extractions).toContain(`hover:font-["Proxima_Nova",_system-ui,_sans-serif]`)
      expect(extractions).toContain(`hover:focus:font-["Inter_var",_system-ui,_sans-serif]`)
    })

    test('arbitrary values with escaped underscores', async () => {
      let extractions = parse(`
      <div class="content-['hello\\_world'] hover:content-['hello\\_world\\_2'] hover:focus:content-['hello\\_world\\_3']"></div>
    `)

      expect(extractions).toContain(`content-['hello\\_world']`)
      expect(extractions).toContain(`hover:content-['hello\\_world\\_2']`)
      expect(extractions).toContain(`hover:focus:content-['hello\\_world\\_3']`)
    })

    test('arbitrary values with angle brackets in single quotes', async () => {
      let extractions = parse(`
      <div class="content-['>'] hover:content-['<'] hover:focus:content-['>']"></div>
    `)

      expect(extractions).toContain(`content-['>']`)
      expect(extractions).toContain(`hover:content-['<']`)
      expect(extractions).toContain(`hover:focus:content-['>']`)
    })

    test('arbitrary values with angle brackets in double quotes', async () => {
      let extractions = parse(`
      <div class="content-[">"] hover:content-["<"] hover:focus:content-[">"]"></div>
    `)

      expect(extractions).toContain(`content-[">"]`)
      expect(extractions).toContain(`hover:content-["<"]`)
      expect(extractions).toContain(`hover:focus:content-[">"]`)
    })

    test('arbitrary values with theme lookup using quotes', () => {
      let extractions = parse(`
      <p class="[--y:theme('colors.blue.500')] [color:var(--y)]"></p>
    `)

      expect(extractions).toContain(`[--y:theme('colors.blue.500')]`)
      expect(extractions).toContain(`[color:var(--y)]`)
    })
  })

  test('special characters', async () => {
    let extractions = parse(`
      <div class="<sm:underline md>:font-bold"></div>
    `)

    expect(extractions).toContain(`<sm:underline`)
    expect(extractions).toContain(`md>:font-bold`)
  })

  test('with single quotes array within template literal', async () => {
    let extractions = parse(`<div class=\`\${['pr-1.5']}\`></div>`)

    expect(extractions).toContain('pr-1.5')
  })

  test('with double quotes array within template literal', async () => {
    let extractions = parse(`<div class=\`\${["pr-1.5"]}\`></div>`)

    expect(extractions).toContain('pr-1.5')
  })

  test('with single quotes array within function', async () => {
    let extractions = parse(`document.body.classList.add(['pl-1.5'].join(" "));`)

    expect(extractions).toContain('pl-1.5')
  })

  test('with double quotes array within function', async () => {
    let extractions = parse(`document.body.classList.add(["pl-1.5"].join(" "));`)

    expect(extractions).toContain('pl-1.5')
  })

  test('with angle brackets', async () => {
    let extractions = parse(
      `<div class="bg-blue-200 <% if (useShadow) { %>shadow-xl<% } %>">test</div>`
    )

    expect(extractions).toContain('bg-blue-200')
    expect(extractions).toContain('shadow-xl')
    expect(extractions).not.toContain('>shadow-xl')
    expect(extractions).not.toContain('shadow-xl<')
  })

  test('markdown code fences', async () => {
    let extractions = parse('<!-- this should work: `.font-bold`, `.font-normal` -->')

    expect(extractions).toContain('font-bold')
    expect(extractions).toContain('font-normal')
    expect(extractions).not.toContain('.font-bold')
    expect(extractions).not.toContain('.font-normal')
  })

  test('classes in slim templates', async () => {
    let extractions = parse(`
      p.bg-red-500.text-sm
        'This is a paragraph
          small.italic.text-gray-500
            '(Look mom, no closing tag!)
    `)

    expect(extractions).toContain('bg-red-500')
    expect(extractions).toContain('text-sm')
    expect(extractions).toContain('italic')
    expect(extractions).toContain('text-gray-500')
  })

  test('multi-word + arbitrary values + quotes', async () => {
    let extractions = parse(`
      grid-cols-['repeat(2)']
    `)

    expect(extractions).toContain(`grid-cols-['repeat(2)']`)
  })

  test('a lot of data', () => {
    let extractions = parse('underline '.repeat(2 ** 17))

    expect(extractions).toContain(`underline`)
  })

  test('ruby percent string array', () => {
    let extractions = parse('%w[text-[#bada55]]')

    expect(extractions).toContain(`text-[#bada55]`)
  })

  test('arbitrary properties followed by square bracketed stuff', () => {
    let extractions = parse(
      '<div class="h-16 items-end border border-white [display:inherit]">[foo]</div>'
    )

    expect(extractions).toContain(`[display:inherit]`)
  })

  describe('Vue', () => {
    test('Class object syntax', () => {
      let extractions = parse(
        `<Examples with combinations :class="{ underline: myCondition, 'font-bold': myCondition }">[foo]</div>`
      )

      expect(extractions).toContain(`underline`, `font-bold`)
    })

    test('Class array syntax', () => {
      // With leading space
      let extractions = parse(
        `<div :class="[ 'underline', myCondition && 'font-bold', myCondition ? 'flex' : 'block' ]">[foo]</div>`
      )

      expect(extractions).toContain(`underline`, `font-bold`, `flex`, `block`)

      // Without leading space
      extractions = parse(
        `<div :class="['underline', myCondition && 'font-bold', myCondition ? 'flex' : 'block']">[foo]</div>`
      )

      expect(extractions).toContain(`underline`, `font-bold`, `flex`, `block`)
    })
  })

  describe('anti-test', () => {
    it('should not parse candidates without spaces in object-like syntax', () => {
      let extractions = parse(html`<div class="{underline:isActive,flex:isOnline,md:bold}"></div>`)

      // The oxide parser _does_ allow this!
      if (parse !== oxideParser) {
        expect(extractions).not.toContain(`underline:isActive`)
        expect(extractions).not.toContain(`flex:isOnline`)
        expect(extractions).not.toContain(`md:bold`)
      }

      expect(extractions).not.toContain(`underline`)
      expect(extractions).not.toContain(`isActive`)
      expect(extractions).not.toContain(`flex`)
      expect(extractions).not.toContain(`isOnline`)
      expect(extractions).not.toContain(`md`)
      expect(extractions).not.toContain(`bold`)
    })
  })

  describe('real world', () => {
    it.each([
      [
        'const myCVAComponent = cva([],{defaultVariants:{size:"md"},variants:{size:{sm:["p-1"],md:["p-1.5"],lg:["p-2"],xl:["p-2.5"]}}});',
      ],
      [
        'const myCVAComponent = cva([],{defaultVariants:{size:"md"}, variants:{size:{sm:["p-1"],md:["p-1.5"],lg:["p-2"],xl:["p-2.5"]}}});',
      ],
      [
        'const myCVAComponent = cva("",{defaultVariants:{size:"md"},variants:{size:{sm:["p-1"],md:["p-1.5"],lg:["p-2"],xl:["p-2.5"]}}});',
      ],
    ])('should work for issue #12109 (%#)', async (content) => {
      let extractions = parse(content)

      expect(extractions).toContain('p-1')
      expect(extractions).toContain('p-1.5')
      expect(extractions).toContain('p-2')
      expect(extractions).toContain('p-2.5')
    })

    it.each([
      // With group name modifier
      [
        '<div class="bg-blue-300 group-[[data-can-play]:not([data-playing])]/parent:bg-red-300 p-4 w-60" ></div>',
        [
          'bg-blue-300',
          'group-[[data-can-play]:not([data-playing])]/parent:bg-red-300',
          'p-4',
          'w-60',
        ],
      ],
      // Without group name modifier
      [
        '<div class="bg-blue-300 group-[[data-can-play]:not([data-playing])]:bg-red-300 p-4 w-60">',
        ['bg-blue-300', 'group-[[data-can-play]:not([data-playing])]:bg-red-300', 'p-4', 'w-60'],
      ],
    ])('should work for issue #12169 (%#)', async (content, expectations) => {
      let extractions = parse(content)

      for (let value of expectations) {
        expect(extractions).toContain(value)
      }
    })

    it.each([
      ['["min-w-[17rem]","max-w-[17rem]"]', ['min-w-[17rem]', 'max-w-[17rem]']],
      [
        '["w-[calc(theme(spacing[2]*-1px))]","h-[calc(theme(spacing[2]*-1px))]"]',
        ['w-[calc(theme(spacing[2]*-1px))]', 'h-[calc(theme(spacing[2]*-1px))]'],
      ],
    ])('should work for issue #12371 (%#)', async (content, expectations) => {
      let extractions = parse(content)

      for (let value of expectations) {
        expect(extractions).toContain(value)
      }
    })
  })
})
