<!DOCTYPE html>

<head>
  <meta charset="utf8">
  <style></style>
</head>

<body>
</body>

<script>
  async function expectThrownError(fn, err) {
    try {
      await fn()
      throw new Error('Expected an error to be thrown')
    } catch (e) {
      assertStrictEqual(e.message, err)
    }
  }

  function assertStrictEqual(a, b, message = 'Assertion failed') {
    if (a !== b) {
      throw new Error(`${message}:
  Observed: ${JSON.stringify(a)}
  Expected: ${JSON.stringify(b)}`);
    }
  }

  async function assertSameColorsWithNestingTransform(esbuild, { css, html }) {
    const crawlColors = node => {
      const { color } = getComputedStyle(node)
      const children = Array.from(node.children).map(crawlColors)
      return children.length ? [color, children] : color
    }
    const style = document.querySelector('style')
    document.body.innerHTML = html
    style.textContent = css
    const original = JSON.stringify(crawlColors(document.body))

    // Test minified CSS
    const minified = await esbuild.transform(css, {
      loader: 'css',
      minify: true,
    })
    style.textContent = minified.code
    assertStrictEqual(original, JSON.stringify(crawlColors(document.body)), 'Compare minified CSS')

    // Test lowered CSS
    const lowered = await esbuild.transform(css, {
      loader: 'css',
      supported: { nesting: false },
    })
    style.textContent = lowered.code
    assertStrictEqual(original, JSON.stringify(crawlColors(document.body)), 'Compare lowered CSS')
  }

  const coreTests = ({ esbuild }) => ({
    async defaultExport() {
      assertStrictEqual(typeof esbuild.version, 'string')
      assertStrictEqual(esbuild.version, esbuild.default.version)
      assertStrictEqual(esbuild.version, esbuild.default.default.version)
      assertStrictEqual(esbuild.version, esbuild.default.default.default.version)
    },

    async transformJS() {
      const { code } = await esbuild.transform('1+2')
      assertStrictEqual(code, '1 + 2;\n')
    },

    async transformTS() {
      const { code } = await esbuild.transform('1 as any + <any>2', { loader: 'ts' })
      assertStrictEqual(code, '1 + 2;\n')
    },

    async transformCSS() {
      const { code } = await esbuild.transform('div { color: red }', { loader: 'css' })
      assertStrictEqual(code, 'div {\n  color: red;\n}\n')
    },

    async buildFib() {
      const fibonacciPlugin = {
        name: 'fib',
        setup(build) {
          build.onResolve({ filter: /^fib\((\d+)\)/ }, args => {
            return { path: args.path, namespace: 'fib' }
          })
          build.onLoad({ filter: /^fib\((\d+)\)/, namespace: 'fib' }, args => {
            let match = /^fib\((\d+)\)/.exec(args.path), n = +match[1]
            let contents = n < 2 ? `export default ${n}` : `
              import n1 from 'fib(${n - 1}) ${args.path}'
              import n2 from 'fib(${n - 2}) ${args.path}'
              export default n1 + n2`
            return { contents }
          })
        },
      }
      const result = await esbuild.build({
        stdin: {
          contents: `
            import x from 'fib(10)'
            module.exports = x
          `,
        },
        format: 'cjs',
        bundle: true,
        plugins: [fibonacciPlugin],
      })
      assertStrictEqual(result.outputFiles.length, 1)
      assertStrictEqual(result.outputFiles[0].path, '<stdout>')
      const code = result.outputFiles[0].text
      const answer = {}
      new Function('module', code)(answer)
      assertStrictEqual(answer.exports, 55)
    },

    async buildRelativeIssue693() {
      const result = await esbuild.build({
        stdin: {
          contents: `const x=1`,
        },
        write: false,
        outfile: 'esbuild.js',
      });
      assertStrictEqual(result.outputFiles.length, 1)
      assertStrictEqual(result.outputFiles[0].path, '/esbuild.js')
      assertStrictEqual(result.outputFiles[0].text, 'const x = 1;\n')
    },

    async watch() {
      const context = await esbuild.context({})
      try {
        await expectThrownError(context.watch, 'Cannot use the "watch" API in this environment')
      } finally {
        context.dispose()
      }
    },

    async serve() {
      const context = await esbuild.context({})
      try {
        await expectThrownError(context.serve, 'Cannot use the "serve" API in this environment')
      } finally {
        context.dispose()
      }
    },

    async esbuildBuildSync() {
      await expectThrownError(esbuild.buildSync, 'The "buildSync" API only works in node')
    },

    async esbuildTransformSync() {
      await expectThrownError(esbuild.transformSync, 'The "transformSync" API only works in node')
    },
  })

  function setupForProblemCSS(prefix) {
    // https://github.com/tailwindlabs/tailwindcss/issues/2889
    const original = `
      /* Variant 1 */
      .${prefix}-v1 { --a: ; --b: ; max-width: var(--a) var(--b); }
      .${prefix}-a { --a: 1px; }
      .${prefix}-b { --b: 2px; }

      /* Variant 2 */
      .${prefix}-v2 { max-width: var(--a, ) var(--b, ); }
      .${prefix}-a { --a: 1px; }
      .${prefix}-b { --b: 2px; }
    `
    const style = document.querySelector('style')
    const test1a = document.createElement('div')
    const test1b = document.createElement('div')
    const test2a = document.createElement('div')
    const test2b = document.createElement('div')
    test1a.className = `${prefix}-v1 ${prefix}-a`
    test1b.className = `${prefix}-v1 ${prefix}-b`
    test2a.className = `${prefix}-v2 ${prefix}-a`
    test2b.className = `${prefix}-v2 ${prefix}-b`
    return [original, css => {
      style.textContent = css
      document.body.innerHTML = ''
      document.body.appendChild(test1a)
      document.body.appendChild(test1b)
      document.body.appendChild(test2a)
      document.body.appendChild(test2b)
      assertStrictEqual(getComputedStyle(test1a).maxWidth, `1px`)
      assertStrictEqual(getComputedStyle(test1b).maxWidth, `2px`)
      assertStrictEqual(getComputedStyle(test2a).maxWidth, `1px`)
      assertStrictEqual(getComputedStyle(test2b).maxWidth, `2px`)
    }]
  }

  const cssTests = ({ esbuild }) => ({
    async problemCSSOriginal() {
      const [original, runAsserts] = setupForProblemCSS('original')
      runAsserts(original)
    },

    async problemCSSPrettyPrinted() {
      const [original, runAsserts] = setupForProblemCSS('pretty-print')
      const { code: prettyPrinted } = await esbuild.transform(original, { loader: 'css' })
      runAsserts(prettyPrinted)
    },

    async problemCSSMinified() {
      const [original, runAsserts] = setupForProblemCSS('pretty-print')
      const { code: minified } = await esbuild.transform(original, { loader: 'css', minify: true })
      runAsserts(minified)
    },

    // See: https://github.com/evanw/esbuild/issues/3877
    async cssNestingIssue3877() {
      await assertSameColorsWithNestingTransform(esbuild, {
        css: `
          .a .b:has(> span) {
            .a & span {
              color: green;
            }
          }
        `,
        html: `
          <div class="a">
            <div class="b">
              <span>1</span>
              <div><span>2</span></div>
            </div>
            <div class="b">
              <div><span>3</span></div>
            </div>
          </div>
        `,
      })
    },

    // See: https://github.com/evanw/esbuild/issues/3877#issuecomment-2631385559
    async cssNestingIssue3877Comment2631385559() {
      await assertSameColorsWithNestingTransform(esbuild, {
        css: `
          .a {
            :has(>&) {
              color: red;
            }
          }
        `,
        html: `
          <div class="a">a</div>
        `,
      })
    },

    // See: https://github.com/evanw/esbuild/issues/3997
    async cssNestingIssue3997() {
      await assertSameColorsWithNestingTransform(esbuild, {
        css: `
          .foo {
            color: blue;
            && { color: red; }
            & { color: green; }
          }
        `,
        html: `
          <div class="foo">x</div>
        `,
      })
    },

    // See: https://github.com/evanw/esbuild/issues/4005
    async cssNestingIssue4005() {
      await assertSameColorsWithNestingTransform(esbuild, {
        css: `
          .foo {
            :where(& > .bar) {
              color: red;
            }
          }
        `,
        html: `
          <div class="foo">
            <div class="bar">bar</div>
          </div>
        `,
      })
    },

    // See: https://github.com/evanw/esbuild/pull/4037
    async cssNestingIssue4037() {
      await assertSameColorsWithNestingTransform(esbuild, {
        css: `
          .parent {
            > .a,
            > .b1 > .b2 {
              color: red;
            }
          }
        `,
        html: `
          <div class="parent">
            <div class="a">a</div>
            <div class="b1">
              <div class="b2">b2</div>
            </div>
          </div>
        `,
      })
    },
  })

  async function runTest(test, fn) {
    try {
      await fn()
    } catch (e) {
      e.test = test
      throw e
    }
  }

  async function loadScript(url) {
    const tag = document.createElement('script')
    document.head.appendChild(tag)
    await new Promise((resolve, reject) => {
      tag.onload = resolve
      tag.onerror = () => reject(new Error('Failed to load script: ' + url))
      tag.src = url
    })
    const esbuild = window.esbuild
    delete window.esbuild
    return esbuild
  }

  async function testStart() {
    let allTestsPassed = true

    if (!window.testBegin) window.testBegin = args => {
      const config = Object.entries(JSON.parse(args)).map(([k, v]) => `${k}=${v}`).join(', ')
      console.log(`💬 config: ${config}`)
    }

    if (!window.testEnd) window.testEnd = args => {
      if (args === null) console.log(`👍 success`)
      else {
        const { test, stack, error } = JSON.parse(args)
        console.log(`❌ error${test ? ` [${test}]` : ``}: ${error}`)
        allTestsPassed = false
      }
    }

    if (!window.testDone) window.testDone = error => {
      console.log(allTestsPassed ? `✅ done` : `❌ done`)
    }

    // Just run CSS tests through a single configuration, but run each test separately
    {
      const url = '/npm/esbuild-wasm/esbuild.wasm?' + Math.random()
      const initializePromise = import('/npm/esbuild-wasm/esm/browser.js?' + Math.random())
        .then(esbuild => esbuild.initialize({ wasmURL: new URL(url, location.href) })
          .then(() => esbuild))
      const tests = cssTests({ esbuild: await initializePromise.catch(() => null) })
      for (const test in tests) {
        try {
          testBegin(JSON.stringify({ type: 'css', test }))
          await initializePromise
          await runTest(test, tests[test])
          testEnd(null)
        } catch (e) {
          testEnd(JSON.stringify({
            test: e.test || null,
            stack: e.stack || null,
            error: (e && e.message || e) + '',
          }))
        }
      }
    }

    // Run all core tests through every configuration, but run all tests together (stop after one failure)
    for (const esm of [false, true]) {
      for (const min of [false, true]) {
        for (const worker of [false, true]) {
          for (const mime of ['correct', 'incorrect']) {
            for (const approach of ['string', 'url', 'module']) {
              try {
                testBegin(JSON.stringify({ esm, min, worker, mime, approach }))
                const esbuild = esm
                  ? await import('/npm/esbuild-wasm/esm/browser' + (min ? '.min' : '') + '.js?' + Math.random())
                  : await loadScript('/npm/esbuild-wasm/lib/browser' + (min ? '.min' : '') + '.js?' + Math.random())
                const url = mime === 'correct' ? '/npm/esbuild-wasm/esbuild.wasm' : '/scripts/browser/esbuild.wasm.bagel'
                const initializePromise = {
                  string: () => esbuild.initialize({ wasmURL: url, worker }),
                  url: () => esbuild.initialize({ wasmURL: new URL(url, location.href), worker }),
                  module: () => fetch(url)
                    .then(r => r.arrayBuffer())
                    .then(bytes => WebAssembly.compile(bytes))
                    .then(module => esbuild.initialize({ wasmModule: module, worker })),
                }[approach]()
                await initializePromise
                const tests = coreTests({ esbuild })
                const promises = []
                for (const test in tests) promises.push(runTest(test, tests[test]))
                await Promise.all(promises)
                testEnd(null)
              } catch (e) {
                testEnd(JSON.stringify({
                  test: e.test || null,
                  stack: e.stack || null,
                  error: (e && e.message || e) + '',
                }))
              }
            }
          }
        }
      }
    }

    testDone()
  }

  testStart()

</script>