let path = require('path')
let $ = require('../../execute')
let { css, html, javascript } = require('../../syntax')
let resolveToolRoot = require('../../resolve-tool-root')

let version = require('../../../package.json').version

let {
  cleanupFile,
  fileExists,
  readOutputFile,
  removeFile,
  waitForOutputFileCreation,
  writeInputFile,
} = require('../../io')({
  output: 'dist',
  input: 'src',
})

let EXECUTABLE = 'node ../../lib/cli.js'

function dedent(input) {
  let lines = input.split('\n')

  let minIndent = lines.reduce(
    (min, line) => Math.min(min, line.trim() === '' ? Infinity : line.match(/^\s*/)[0].length),
    Infinity
  )

  return lines
    .map((line) => line.slice(minIndent))
    .join('\n')
    .trim()
}

describe('Build command', () => {
  test('--output', async () => {
    await writeInputFile('index.html', html`<div class="font-bold shadow"></div>`)

    await $(`${EXECUTABLE} --output ./dist/main.css`)

    let contents = await readOutputFile('main.css')

    // `-i` is omitted, therefore the default `@tailwind base; @tailwind
    // components; @tailwind utilities` is used. However `preflight` is
    // disabled. I still want to verify that the `base` got included.
    expect(contents).toContain('--tw-ring-offset-shadow: 0 0 #0000')
    expect(contents).toContain('--tw-ring-shadow: 0 0 #0000')
    expect(contents).toContain('--tw-shadow: 0 0 #0000')

    // Verify `utilities` output is correct
    expect(contents).toIncludeCss(
      css`
        .font-bold {
          font-weight: 700;
        }
      `
    )
  })

  test('--input, --output', async () => {
    await writeInputFile('index.html', html`<div class="font-bold"></div>`)

    await $(`${EXECUTABLE} --input ./src/index.css --output ./dist/main.css`)

    expect(await readOutputFile('main.css')).toIncludeCss(
      css`
        .font-bold {
          font-weight: 700;
        }
      `
    )
  })

  test('--minify', async () => {
    await writeInputFile('index.html', html`<div class="font-bold"></div>`)

    await $(`${EXECUTABLE} --output ./dist/main.css --minify`)
    let withMinify = await readOutputFile('main.css')

    // Verify that we got the expected output. Note: `.toIncludeCss` formats
    // `actual` & `expected`
    expect(withMinify).toIncludeCss(
      css`
        .font-bold {
          font-weight: 700;
        }
      `
    )

    await $(`${EXECUTABLE} --output ./dist/main.css`)
    let withoutMinify = await readOutputFile('main.css')

    // Let's verify that the actual minified output is smaller than the not
    // minified version.
    expect(withoutMinify.length).toBeGreaterThan(withMinify.length)
  })

  // Deprecated
  test('--no-autoprefixer (should produce a warning)', async () => {
    await writeInputFile('index.html', html`<div class="select-none"></div>`)
    await writeInputFile(
      'index.css',
      css`
        @tailwind utilities;
      `
    )

    let runningProcess = $(
      `${EXECUTABLE} --input ./src/index.css --output ./dist/main.css --no-autoprefixer`
    )
    let warning = runningProcess.onStderr((message) => message.includes('--no-autoprefixer'))
    await runningProcess
    expect(await warning).toMatchInlineSnapshot(`
      "[deprecation] The --no-autoprefixer flag is deprecated and has no effect.
      "
    `)
    let withoutAutoprefixer = await readOutputFile('main.css')

    // This contains --webkit-user-select which may be strange, but it is expected because we are
    // not handling the `--no-autoprefixer` flag anymore at all.
    expect(withoutAutoprefixer).toMatchInlineSnapshot(`
      "/* ! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com */
      .select-none {
        -webkit-user-select: none;
        user-select: none;
      }
      "
    `)
  })

  test('--config (non-existing config file)', async () => {
    await writeInputFile('index.html', html`<div class="font-bold"></div>`)

    let { stderr } = await $(
      `${EXECUTABLE} --output ./dist/main.css --config ./non-existing.config.js`
    ).catch((err) => err)

    let toolRoot = resolveToolRoot()
    expect(stderr).toEqual(
      `Specified config file ${path.resolve(toolRoot, 'non-existing.config.js')} does not exist.\n`
    )
  })

  test('--config (existing config file)', async () => {
    await writeInputFile('index.html', html`<div class="font-bold"></div>`)

    let customConfig = `module.exports = ${JSON.stringify(
      {
        content: ['./src/index.html'],
        theme: {
          extend: {
            fontWeight: {
              bold: 'bold',
            },
          },
        },
        corePlugins: {
          preflight: false,
        },
        plugins: [],
      },

      null,
      2
    )}`

    await writeInputFile('../custom.config.js', customConfig)

    await $(`${EXECUTABLE} --output ./dist/main.css --config ./custom.config.js`)

    expect(await readOutputFile('main.css')).toIncludeCss(
      css`
        .font-bold {
          font-weight: bold;
        }
      `
    )
  })

  test('--content', async () => {
    await writeInputFile('other.html', html`<div class="font-bold"></div>`)

    await $(`${EXECUTABLE} --content ./src/other.html --output ./dist/main.css`)

    expect(await readOutputFile('main.css')).toIncludeCss(
      css`
        .font-bold {
          font-weight: 700;
        }
      `
    )
  })

  test('--postcss (postcss.config.js)', async () => {
    await writeInputFile('index.html', html`<div class="font-bold"></div>`)

    let customConfig = javascript`
      let path = require('path')
      let postcss = require('postcss')

      module.exports = {
        plugins: [
          function before(root, result) {
            // Inject a custom component with @apply rules to prove that we run
            // this _before_ the actual tailwind plugin.
            let btn = postcss.parse('.btn { @apply flex px-2 py-1 }')
            root.append(btn.nodes)
          },
          function tailwindcss() {
            return require(path.resolve('..', '..'))
          },
          function after(root, result) {
            // Add '-after' to all the selectors
            root.walkRules(rule => {
              if (!rule.selector.startsWith('.')) return
              rule.selector = rule.selector + '-after'
            })
          },
        ],
      }
    `

    await writeInputFile('../postcss.config.js', customConfig)

    await $(`${EXECUTABLE} --output ./dist/main.css --postcss`)

    expect(await readOutputFile('main.css')).toIncludeCss(
      css`
        .font-bold-after {
          font-weight: 700;
        }

        .btn-after {
          padding: 0.25rem 0.5rem;
          display: flex;
        }
      `
    )
  })

  test('--postcss (custom.postcss.config.js)', async () => {
    await writeInputFile('index.html', html`<div class="font-bold"></div>`)

    let customConfig = javascript`
      let path = require('path')
      let postcss = require('postcss')

      module.exports = {
        plugins: [
          function before(root, result) {
            // Inject a custom component with @apply rules to prove that we run
            // this _before_ the actual tailwind plugin.
            let btn = postcss.parse('.btn { @apply flex px-2 py-1 }')
            root.append(btn.nodes)
          },
          function tailwindcss() {
            return require(path.resolve('..', '..'))
          },
          function after(root, result) {
            // Add '-after' to all the selectors
            root.walkRules(rule => {
              if (!rule.selector.startsWith('.')) return
              rule.selector = rule.selector + '-after'
            })
          },
        ],
      }
    `

    await writeInputFile('../custom.postcss.config.js', customConfig)

    await $(`${EXECUTABLE} --output ./dist/main.css --postcss ./custom.postcss.config.js`)

    expect(await readOutputFile('main.css')).toIncludeCss(
      css`
        .font-bold-after {
          font-weight: 700;
        }

        .btn-after {
          padding: 0.25rem 0.5rem;
          display: flex;
        }
      `
    )
  })

  test('--postcss supports process options', async () => {
    await writeInputFile('index.html', html`<div class="font-bold"></div>`)

    let customConfig = javascript`
      let path = require('path')
      let postcss = require('postcss')

      module.exports = {
        map: { inline: true },
        plugins: [
          function tailwindcss() {
            return require(path.resolve('..', '..'))
          },
        ],
      }
    `

    await writeInputFile('../postcss.config.js', customConfig)

    await $(`${EXECUTABLE} --output ./dist/main.css --postcss`)

    let contents = await readOutputFile('main.css')

    expect(contents).toIncludeCss(
      css`
        .font-bold {
          font-weight: 700;
        }
      `
    )

    expect(contents).toContain(`/*# sourceMappingURL`)
  })

  test('--postcss supports process options with custom config', async () => {
    await writeInputFile('index.html', html`<div class="font-bold"></div>`)

    let customConfig = javascript`
      let path = require('path')
      let postcss = require('postcss')

      module.exports = {
        map: { inline: true },
        plugins: [
          function tailwindcss() {
            return require(path.resolve('..', '..'))
          },
        ],
      }
    `

    await writeInputFile('../custom.postcss.config.js', customConfig)

    await $(`${EXECUTABLE} --output ./dist/main.css --postcss ./custom.postcss.config.js`)

    let contents = await readOutputFile('main.css')

    expect(contents).toIncludeCss(
      css`
        .font-bold {
          font-weight: 700;
        }
      `
    )

    expect(contents).toContain(`/*# sourceMappingURL`)
  })

  test('postcss-import is supported by default', async () => {
    cleanupFile('src/test.css')

    await writeInputFile('index.html', html`<div class="md:something-cool"></div>`)
    await writeInputFile(
      'test.css',
      css`
        @import 'tailwindcss/base';
        @import 'tailwindcss/components';
        @import 'tailwindcss/utilities';
        @import './imported.css';
      `
    )

    await $(
      `${EXECUTABLE} --input ./src/test.css --content ./src/index.html --output ./dist/main.css`
    )

    expect(await readOutputFile('main.css')).toIncludeCss(
      css`
        @media (min-width: 768px) {
          .md\:something-cool {
            color: red;
          }
        }
      `
    )
  })

  test('postcss-import is supported by default in watch mode', async () => {
    cleanupFile('src/test.css')

    await writeInputFile('index.html', html`<div class="md:something-cool"></div>`)
    await writeInputFile(
      'test.css',
      css`
        @import 'tailwindcss/base';
        @import 'tailwindcss/components';
        @import 'tailwindcss/utilities';
        @import './imported.css';
      `
    )

    let runningProcess = $(
      `${EXECUTABLE} --watch --input ./src/test.css --content ./src/index.html --output ./dist/main.css`
    )

    await waitForOutputFileCreation('main.css')

    expect(await readOutputFile('main.css')).toIncludeCss(
      css`
        @media (min-width: 768px) {
          .md\:something-cool {
            color: red;
          }
        }
      `
    )

    await writeInputFile(
      'imported.css',
      css`
        @layer utilities {
          .something-cool {
            color: blue;
          }
        }
      `
    )

    await runningProcess.onStderr(function ready(message) {
      return message.includes('Done in')
    })

    expect(await readOutputFile('main.css')).toIncludeCss(
      css`
        @media (min-width: 768px) {
          .md\:something-cool {
            color: #00f;
          }
        }
      `
    )

    return runningProcess.stop()
  })

  test('postcss-import is included when using a custom postcss configuration', async () => {
    cleanupFile('src/test.css')

    await writeInputFile('index.html', html`<div class="md:something-cool"></div>`)
    await writeInputFile(
      'imported.css',
      css`
        .foo {
          color: white;
        }
      `
    )
    await writeInputFile(
      'test.css',
      css`
        @import 'tailwindcss/base';
        @import 'tailwindcss/components';
        @import 'tailwindcss/utilities';
        @import './imported.css';
      `
    )

    await $(
      `${EXECUTABLE} --input ./src/test.css --content ./src/index.html --output ./dist/main.css --postcss`
    )

    expect(await readOutputFile('main.css')).toIncludeCss(
      css`
        .foo {
          color: #fff;
        }
      `
    )
  })

  test('--help', async () => {
    let { combined } = await $(`${EXECUTABLE} --help`)

    expect(dedent(combined)).toEqual(
      dedent(`
          tailwindcss v${version}

          Usage:
             tailwindcss build [options]

          Options:
             -i, --input              Input file
             -o, --output             Output file
             -w, --watch              Watch for changes and rebuild as needed
             -p, --poll               Use polling instead of filesystem events when watching
                 --content            Content paths to use for removing unused classes
                 --postcss            Load custom PostCSS configuration
             -m, --minify             Minify the output
             -c, --config             Path to a custom config file
             -h, --help               Display usage information
        `)
    )
  })
})

describe('Init command', () => {
  it.each([
    { flags: [], name: 'tailwind.config.js' },
    { flags: ['--ts'], name: 'tailwind.config.ts' },
    { flags: ['--esm'], name: 'tailwind.config.js' },
    { flags: ['--full'], name: 'tailwind.config.js' },
    { flags: ['--ts', '--full'], name: 'tailwind.config.ts' },
    { flags: ['--esm', '--full'], name: 'tailwind.config.js' },
  ])('works with all these flags: %j', async ({ flags, name }) => {
    await removeFile(name)

    let { combined } = await $(`${EXECUTABLE} init ${flags.join(' ')}`)

    expect(combined).toMatchInlineSnapshot(`
      "
      Created Tailwind CSS config file: ${name}
      "
    `)

    expect(await fileExists(name)).toBe(true)

    let content = await readOutputFile(`../${name}`)

    if (flags.includes('--ts') || flags.includes('--esm')) {
      expect(content).toContain('export default')
      expect(content).not.toContain('module.exports =')
    } else {
      expect(content).toContain('module.exports =')
      expect(content).not.toContain('export default')
    }

    if (flags.includes('--ts')) {
      expect(content).toContain('satisfies Config')
    }

    if (flags.includes('--full')) {
      expect(content.split('\n').length).toBeGreaterThan(50)
    }
  })

  test('--full', async () => {
    cleanupFile('full.config.js')

    let { combined } = await $(`${EXECUTABLE} init full.config.js --full`)

    expect(combined).toMatchInlineSnapshot(`
      "
      Created Tailwind CSS config file: full.config.js
      "
    `)

    // Not a clean way to test this. We could require the file and verify that
    // multiple keys in `theme` exists. However it loads `tailwindcss/colors`
    // which doesn't exists in this context.
    expect((await readOutputFile('../full.config.js')).split('\n').length).toBeGreaterThan(50)
  })

  test('--postcss', async () => {
    expect(await fileExists('postcss.config.js')).toBe(true)
    await removeFile('postcss.config.js')
    expect(await fileExists('postcss.config.js')).toBe(false)

    let { combined } = await $(`${EXECUTABLE} init --postcss`)

    expect(await fileExists('postcss.config.js')).toBe(true)

    expect(combined).toMatchInlineSnapshot(`
      "
      tailwind.config.js already exists.
      Created PostCSS config file: postcss.config.js
      "
    `)
  })

  test('--help', async () => {
    let { combined } = await $(`${EXECUTABLE} init --help`)

    expect(dedent(combined)).toEqual(
      dedent(`
          tailwindcss v${version}

          Usage:
             tailwindcss init [options]

          Options:
                 --esm                Initialize configuration file as ESM
                 --ts                 Initialize configuration file as TypeScript
             -p, --postcss            Initialize a \`postcss.config.js\` file
             -f, --full               Include the default values for all options in the generated configuration file
             -h, --help               Display usage information
        `)
    )
  })

  test('ESM config is created by default in an ESM project', async () => {
    await removeFile('tailwind.config.js')

    let pkg = await readOutputFile('../package.json')

    await writeInputFile(
      '../package.json',
      JSON.stringify({
        ...JSON.parse(pkg),
        type: 'module',
      })
    )

    let { combined } = await $(`${EXECUTABLE} init`)

    expect(combined).toMatchInlineSnapshot(`
      "
      Created Tailwind CSS config file: tailwind.config.js
      "
    `)

    expect(await fileExists('./tailwind.config.js')).toBe(true)

    // Not a clean way to test this.
    expect(await readOutputFile('../tailwind.config.js')).toContain('export default')

    await writeInputFile('../package.json', pkg)
  })

  test('CJS config is created by default in a non-ESM project', async () => {
    await removeFile('tailwind.config.js')

    let pkg = await readOutputFile('../package.json')

    await writeInputFile(
      '../package.json',
      JSON.stringify({
        ...JSON.parse(pkg),
      })
    )

    let { combined } = await $(`${EXECUTABLE} init`)

    expect(combined).toMatchInlineSnapshot(`
      "
      Created Tailwind CSS config file: tailwind.config.js
      "
    `)

    expect(await fileExists('./tailwind.config.js')).toBe(true)

    // Not a clean way to test this.
    expect(await readOutputFile('../tailwind.config.js')).toContain('module.exports')

    await writeInputFile('../package.json', pkg)
  })
})
