/** @typedef {import('rollup').OutputChunk} OutputChunk */
/** @typedef {import('rollup').OutputAsset} OutputAsset */
/** @typedef {import('rollup').InputOptions} InputOptions */
/** @typedef {import('../rollup-plugin-html').RollupPluginHtml} RollupPluginHtml */
/** @typedef {(OutputChunk | OutputAsset)[]} Output */

const rollup = require('rollup');
const { expect } = require('chai');
const htmlPlugin = require('../rollup-plugin-html');

/**
 * @param {Output} output
 * @param {string} name
 * @returns {OutputChunk}
 */
function getChunk(output, name) {
  return /** @type {OutputChunk} */ (output.find(o => o.fileName === name && o.type === 'chunk'));
}

/**
 * @param {Output} output
 * @param {string} name
 * @returns {OutputAsset & { source: string }}
 */
function getAsset(output, name) {
  return /** @type {OutputAsset & { source: string }} */ (output.find(
    o => o.fileName === name && o.type === 'asset',
  ));
}

/** @type {any} */
const outputConfig = {
  format: 'es',
  dir: 'dist',
};

/** @param {string} str */
function stripNewlines(str) {
  return str.replace(/(\r\n|\n|\r)/gm, '');
}

describe('rollup-plugin-html', () => {
  it('can build an app with rollup bundle injected into a default HTML page and filename', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        htmlPlugin({
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    expect(getChunk(output, 'entrypoint-a.js').code).to.include("console.log('entrypoint-a.js');");
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '</body></html>',
    );
  });

  it('can build with html file as input', async () => {
    const config = {
      plugins: [
        htmlPlugin({
          inputPath: require.resolve('./fixtures/rollup-plugin-html/index.html'),
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><h1>hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '</body></html>',
    );
  });

  it('can build with html file as rollup input', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/index.html'),
      plugins: [
        htmlPlugin({
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><h1>hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '</body></html>',
    );
  });

  it('can build with a html string as input', async () => {
    const config = {
      plugins: [
        htmlPlugin({
          name: 'index.html',
          inputHtml:
            '<h1>Hello world</h1>' +
            '<script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with inline modules', async () => {
    const config = {
      plugins: [
        htmlPlugin({
          name: 'index.html',
          inputHtml:
            '<h1>Hello world</h1>' +
            '<script type="module">import "./test/fixtures/rollup-plugin-html/entrypoint-a.js";</script>',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    const { code: appCode } = getChunk(output, 'inline-module-index-0.js');
    expect(appCode).to.include("console.log('entrypoint-a.js');");
    expect(stripNewlines(getAsset(output, 'index.html').source)).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="./inline-module-index-0.js"></script>' +
        '</body></html>',
    );
  });

  it('resolves inline module imports relative to the HTML file', async () => {
    const config = {
      plugins: [
        htmlPlugin({ inputPath: 'test/fixtures/rollup-plugin-html/foo/foo.html', minify: false }),
      ],
    };

    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    const { code: appCode } = getChunk(output, 'inline-module-foo-0.js');
    expect(appCode).to.include("console.log('foo');");
  });

  it('can build with js input and generated html output', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        htmlPlugin({
          name: 'index.html',
          inject: false,
          template({ bundle }) {
            return `<h1>Hello world</h1>${bundle.entrypoints.map(
              e => `<script type="module" src="${e.importPath}"></script>`,
            )}`;
          },
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    expect(getAsset(output, 'index.html').source).to.equal(
      '<h1>Hello world</h1><script type="module" src="./entrypoint-a.js"></script>',
    );
  });

  it('can build transforming final output', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        htmlPlugin({
          name: 'index.html',
          inputHtml:
            '<h1>Hello world</h1>' +
            '<script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
          transform(html) {
            return html.replace('Hello world', 'Goodbye world');
          },
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    expect(getAsset(output, 'index.html').source).to.equal(
      '<html><head></head><body><h1>Goodbye world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with a public path', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        htmlPlugin({
          name: 'index.html',
          inputHtml:
            '<h1>Hello world</h1>' +
            '<script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
          publicPath: '/static/',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    expect(getAsset(output, 'index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="/static/entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with a public path with a file in a directory', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        htmlPlugin({
          name: 'pages/index.html',
          inputHtml:
            '<h1>Hello world</h1>' +
            '<script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
          publicPath: '/static/',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    expect(getAsset(output, 'pages/index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="/static/entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with multiple build outputs', async () => {
    const plugin = htmlPlugin({
      name: 'index.html',
      inputHtml:
        '<h1>Hello world</h1>' +
        '<script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
      publicPath: '/static/',
      minify: false,
    });

    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [plugin],
    };

    const build = await rollup.rollup(config);
    const bundleA = build.generate({
      format: 'system',
      dir: 'dist',
      plugins: [plugin.addOutput('legacy')],
    });
    const bundleB = build.generate({
      format: 'es',
      dir: 'dist',
      plugins: [plugin.addOutput('modern')],
    });

    const { output: outputA } = await bundleA;
    const { output: outputB } = await bundleB;

    expect(outputA.length).to.equal(1);
    expect(outputB.length).to.equal(2);
    const { code: entrypointA1 } = getChunk(outputA, 'entrypoint-a.js');
    const { code: entrypointA2 } = getChunk(outputB, 'entrypoint-a.js');
    expect(entrypointA1).to.include("console.log('entrypoint-a.js');");
    expect(entrypointA1).to.include("console.log('module-a.js');");
    expect(entrypointA2).to.include("console.log('entrypoint-a.js');");
    expect(entrypointA2).to.include("console.log('module-a.js');");
    expect(getAsset(outputA, 'index.html')).to.not.exist;
    expect(getAsset(outputB, 'index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script>System.import("/static/entrypoint-a.js");</script>' +
        '<script type="module" src="/static/entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with multiple build outputs, specifying a specific output directory', async () => {
    const plugin = htmlPlugin({
      name: 'index.html',
      outputBundleName: 'modern',
      inputHtml:
        '<h1>Hello world</h1>' +
        '<script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
      publicPath: '/static/',
      minify: false,
    });

    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [plugin],
    };

    const build = await rollup.rollup(config);
    const bundleA = build.generate({
      format: 'system',
      dir: 'dist-1',
      plugins: [plugin.addOutput('legacy')],
    });
    const bundleB = build.generate({
      format: 'es',
      dir: 'dist-2',
      plugins: [plugin.addOutput('modern')],
    });

    const [{ output: outputA }, { output: outputB }] = await Promise.all([bundleA, bundleB]);

    expect(outputA.length).to.equal(1);
    expect(outputB.length).to.equal(2);
    const { code: entrypointA1 } = getChunk(outputA, 'entrypoint-a.js');
    const { code: entrypointA2 } = getChunk(outputB, 'entrypoint-a.js');
    expect(entrypointA1).to.include("console.log('entrypoint-a.js');");
    expect(entrypointA1).to.include("console.log('module-a.js');");
    expect(entrypointA2).to.include("console.log('entrypoint-a.js');");
    expect(entrypointA2).to.include("console.log('module-a.js');");
    expect(getAsset(outputA, 'index.html')).to.not.exist;
    expect(getAsset(outputB, 'index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script>System.import("/dist-1/entrypoint-a.js");</script>' +
        '<script type="module" src="/static/entrypoint-a.js"></script></body></html>',
    );
  });

  it('can build with index.html as input and an extra html file as output', async () => {
    const config = {
      plugins: [
        htmlPlugin({
          name: 'index.html',
          inputHtml:
            '<h1>Hello world</h1>' +
            '<script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
          minify: false,
        }),

        htmlPlugin({
          name: 'foo.html',
          template: '<html><body><h1>foo.html</h1></body></html>',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(3);
    expect(getChunk(output, 'entrypoint-a.js')).to.exist;
    expect(getAsset(output, 'index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script></body></html>',
    );
    expect(getAsset(output, 'foo.html').source).to.equal(
      '<html><head></head><body><h1>foo.html</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '</body></html>',
    );
  });

  it('can build with html file as input', async () => {
    const config = {
      plugins: [
        htmlPlugin({
          inputPath: 'test/fixtures/rollup-plugin-html/index.html',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(4);
    const { code: entryA } = getChunk(output, 'entrypoint-a.js');
    const { code: entryB } = getChunk(output, 'entrypoint-b.js');
    expect(entryA).to.include("console.log('entrypoint-a.js');");
    expect(entryB).to.include("console.log('entrypoint-b.js');");
    expect(getAsset(output, 'index.html').source).to.equal(
      '<html><head></head><body><h1>hello world</h1>\n\n' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<script type="module" src="./entrypoint-b.js"></script>' +
        '</body></html>',
    );
  });

  it('can build with js input, injecting the same bundle into multiple html files', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        htmlPlugin({
          name: 'page-a.html',
          template: '<h1>Page A</h1>',
          minify: false,
        }),

        htmlPlugin({
          name: 'page-b.html',
          template: '<h1>Page B</h1>',
          minify: false,
        }),

        htmlPlugin({
          name: 'page-c.html',
          template: '<h1>Page C</h1>',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(4);
    expect(getChunk(output, 'entrypoint-a.js')).to.exist;
    expect(getAsset(output, 'page-a.html').source).to.equal(
      '<html><head></head><body><h1>Page A</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '</body></html>',
    );
    expect(getAsset(output, 'page-b.html').source).to.equal(
      '<html><head></head><body><h1>Page B</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '</body></html>',
    );
    expect(getAsset(output, 'page-c.html').source).to.equal(
      '<html><head></head><body><h1>Page C</h1>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '</body></html>',
    );
  });

  it('can build with multiple html inputs', async () => {
    const config = {
      plugins: [
        htmlPlugin({
          name: 'page-a.html',
          inputHtml:
            '<h1>Page A</h1><script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
          minify: false,
        }),
        htmlPlugin({
          name: 'page-b.html',
          inputHtml:
            '<h1>Page B</h1><script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-b.js"></script>',
          minify: false,
        }),
        htmlPlugin({
          name: 'page-c.html',
          inputHtml:
            '<h1>Page C</h1><script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-c.js"></script>',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(7);
    expect(getChunk(output, 'entrypoint-a.js')).to.exist;
    expect(getChunk(output, 'entrypoint-b.js')).to.exist;
    expect(getChunk(output, 'entrypoint-c.js')).to.exist;
    expect(getAsset(output, 'page-a.html').source).to.equal(
      '<html><head></head><body><h1>Page A</h1><script type="module" src="./entrypoint-a.js"></script></body></html>',
    );
    expect(getAsset(output, 'page-b.html').source).to.equal(
      '<html><head></head><body><h1>Page B</h1><script type="module" src="./entrypoint-b.js"></script></body></html>',
    );
    expect(getAsset(output, 'page-c.html').source).to.equal(
      '<html><head></head><body><h1>Page C</h1><script type="module" src="./entrypoint-c.js"></script></body></html>',
    );
  });

  it('outputs the hashed entrypoint name', async () => {
    const config = {
      plugins: [
        htmlPlugin({
          name: 'index.html',
          inputHtml:
            '<h1>Hello world</h1>' +
            '<script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate({
      ...outputConfig,
      entryFileNames: '[name]-[hash].js',
    });

    expect(output.length).to.equal(2);
    const entrypoint = /** @type {OutputChunk} */ (output.find(f =>
      // @ts-ignore
      f.facadeModuleId.endsWith('entrypoint-a.js'),
    ));
    // ensure it's actually hashed
    expect(entrypoint.fileName).to.not.equal('entrypoint-a.js');
    // get hashed name dynamically
    expect(getAsset(output, 'index.html').source).to.equal(
      `<html><head></head><body><h1>Hello world</h1><script type="module" src="./${entrypoint.fileName}"></script></body></html>`,
    );
  });

  it('outputs import path relative to the final output html', async () => {
    const config = {
      plugins: [
        htmlPlugin({
          name: 'pages/index.html',
          inputHtml:
            '<h1>Hello world</h1>' +
            '<script type="module" src="./test/fixtures/rollup-plugin-html/entrypoint-a.js"></script>',
          minify: false,
        }),
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    expect(getAsset(output, 'pages/index.html').source).to.equal(
      '<html><head></head><body><h1>Hello world</h1><script type="module" src="../entrypoint-a.js"></script></body></html>',
    );
  });

  it('can get the filename with getHtmlFileName()', async () => {
    // default filename
    const pluginA = htmlPlugin({ inputHtml: 'Hello world' });
    // filename inferred from input filename
    const pluginB = htmlPlugin({ inputPath: 'test/fixtures/rollup-plugin-html/my-page.html' });
    // filename explicitly set
    const pluginC = htmlPlugin({
      name: 'pages/my-other-page.html',
      inputPath: 'test/fixtures/rollup-plugin-html/index.html',
    });

    await rollup.rollup({
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [pluginA],
    });
    await rollup.rollup({ plugins: [pluginB] });
    await rollup.rollup({ plugins: [pluginC] });

    expect(pluginA.getHtmlFileName()).to.equal('index.html');
    expect(pluginB.getHtmlFileName()).to.equal('my-page.html');
    expect(pluginC.getHtmlFileName()).to.equal('pages/my-other-page.html');
  });

  it('supports other plugins injecting a transform function', async () => {
    const config = {
      input: require.resolve('./fixtures/rollup-plugin-html/entrypoint-a.js'),
      plugins: [
        htmlPlugin({
          name: 'my-page.html',
          minify: false,
        }),
        {
          name: 'other-plugin',
          /** @param {InputOptions} options */
          buildStart(options) {
            if (!options.plugins) throw new Error('no plugins');
            const p = /** @type {RollupPluginHtml} */ (options.plugins.find(pl => {
              if (pl.name === '@open-wc/rollup-plugin-html') {
                const pl2 = /** @type {RollupPluginHtml} */ (pl);
                return pl2.getHtmlFileName() === 'my-page.html';
              }
              return false;
            }));

            p.addHtmlTransformer(html => html.replace('</body>', '<!-- injected --></body>'));
          },
        },
      ],
    };
    const bundle = await rollup.rollup(config);
    const { output } = await bundle.generate(outputConfig);

    expect(output.length).to.equal(2);
    expect(getChunk(output, 'entrypoint-a.js').code).to.include("console.log('entrypoint-a.js');");
    expect(getAsset(output, 'my-page.html').source).to.equal(
      '<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>' +
        '<script type="module" src="./entrypoint-a.js"></script>' +
        '<!-- injected --></body></html>',
    );
  });
});
