import {MinifierOptions} from 'html-minifier-next';
import MagicString, {SourceMapOptions} from 'magic-string';
import * as assert from 'node:assert/strict';
import {afterEach, beforeEach, describe as suite, test} from 'node:test';
import Sinon from 'sinon';
import {Template, TemplatePart} from '../lib/models.js';
import {ParseLiteralsOptions, parseLiterals} from '../lib/parse-literals.js';

import {
  SourceMap,
  defaultGenerateSourceMap,
  defaultShouldMinify,
  defaultShouldMinifyCSS,
  defaultValidation,
  minifyHTMLLiterals,
} from '../lib/minify-html-literals.js';
import {defaultMinifyOptions, defaultStrategy} from '../lib/strategy.js';

class MagicStringLike {
  generateMap(options?: Partial<SourceMapOptions>): SourceMap {
    return {
      version: 3,
      file: (options && options.file) || null,
      sources: [(options && options.source) || null],
      sourcesContent: [],
      names: [],
      mappings: '',
      toString() {
        return '';
      },
      toUrl() {
        return '';
      },
    };
  }

  overwrite(_start: number, _end: number, _content: string): void {
    // noop
  }

  toString(): string {
    return '';
  }
}

suite('minify-html-literals', () => {
  const SOURCE = `
    function render(title, items, styles) {
      return html\`
        <style>
          \${styles}
        </style>
        <h1 class="heading">\${title}</h1>
        <button onclick="\${() => eventHandler()}"></button>
        <ul>
          \${items.map(item => {
            return getHTML()\`
              <li>\${item}</li>
            \`;
          })}
        </ul>
      \`;
    }

    function noMinify() {
      return \`
        <div>Not tagged html</div>
      \`;
    }

    function taggednoMinify(extra) {
      return other\`
        <style>
          .heading {
            font-size: 24px;
          }

          \${extra}
        </style>
      \`;
    }

    function taggedCSSMinify(extra) {
      return css\`
        .heading {
          font-size: 24px;
        }

        \${extra}
      \`;
    }

    function cssProperty(property) {
      const width = '20px';
      return css\`
        .foo {
          font-size: 1rem;
          width: \${width};
          color: \${property};
        }
      \`;
    }
  `;

  const SOURCE_MIN = `
    function render(title, items, styles) {
      return html\`<style>\${styles}</style><h1 class="heading">\${title}</h1><button onclick="\${() => eventHandler()}"></button><ul>\${items.map(item => {
            return getHTML()\`<li>\${item}</li>\`;
          })}</ul>\`;
    }

    function noMinify() {
      return \`
        <div>Not tagged html</div>
      \`;
    }

    function taggednoMinify(extra) {
      return other\`
        <style>
          .heading {
            font-size: 24px;
          }

          \${extra}
        </style>
      \`;
    }

    function taggedCSSMinify(extra) {
      return css\`.heading{font-size:24px}\${extra}\`;
    }

    function cssProperty(property) {
      const width = '20px';
      return css\`.foo{width:\${width};color:\${property};font-size:1rem}\`;
    }
  `;

  const STATIC_SOURCE = `
    function render() {
      const tagName = literal\`span\`
      return html\`
        <\${tagName}>
        span content
        </\${tagName}>
      \`;
    }
  `;

  const SVG_SOURCE = `
    function taggedSVGMinify() {
      return svg\`
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
          <path d="M6 19h12v2H6z" />
          <path d="M0 0h24v24H0V0z" fill="none" />
        </svg>
      \`;
    }
  `;

  const SVG_SOURCE_MIN = `
    function taggedSVGMinify() {
      return svg\`<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M6 19h12v2H6z"/><path d="M0 0h24v24H0V0z" fill="none"/></svg>\`;
    }
  `;

  const COMMENT_SOURCE = `
    function minifyWithComment() {
      return html\`
        <div .icon=\${0/*JS Comment */}>
        </div>
      \`;
    }
  `;

  const COMMENT_SOURCE_MIN = `
    function minifyWithComment() {
      return html\`<div .icon="\${0/*JS Comment */}"></div>\`;
    }
  `;

  const SVG_MULTILINE_SOURCE = `
    function multiline() {
      return html\`
        <pre>
          Keep newlines

          within certain tags
        </pre>
        <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
          <path d="M6 19h12v2H6z" />
          <path d="M0
                   0h24v24H0V0z"
                fill="none" />
        </svg>
      \`;
    }
  `;

  const SVG_MULTILINE_SOURCE_MIN = `
    function multiline() {
      return html\`<pre>
          Keep newlines

          within certain tags
        </pre><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M6 19h12v2H6z"/><path d="M0                   0h24v24H0V0z" fill="none"/></svg>\`;
    }
  `;

  const SHADOW_PARTS_SOURCE = `
    function parts() {
      return css\`
        foo-bar::part(space separated) {
          color: red;
        }
      \`;
    }
  `;

  const SHADOW_PARTS_SOURCE_MIN = `
    function parts() {
      return css\`foo-bar::part(space separated){color:red}\`;
    }
  `;

  const MEMBER_EXPRESSION_LITERAL_SOURCE = `
    function nested() {
      return LitHtml.html\`<div id="container">
        <span>Some content here</span>
      </div>
      \`;
    }
  `;

  const MEMBER_EXPRESSION_LITERAL_SOURCE_MIN = `
    function nested() {
      return LitHtml.html\`<div id="container"><span>Some content here</span></div>\`;
    }
  `;
  const CONTAINER_QUERY_SOURCE = `
    function someFunction() {
      return css\`
        :host {
          container-type: inline-size;
        }

        .some-class {
          padding: 16px;
        }
      
        @container (min-width: 768px) {
          .some-class {
            display: flex;
          }

          .another-class {
            font-size: 20px;
          }
        }
      \`;
    }
  `;

  const CONTAINER_QUERY_SOURCE_MIN = `
    function someFunction() {
      return css\`:host{container-type:inline-size}.some-class{padding:16px}@container (width>=768px){.some-class{display:flex}.another-class{font-size:20px}}\`;
    }
  `;

  const CSS_TEMPLATE_EXPRESSION_SOURCE = `
    function getSomeCss(){
      const width = '20px';
      const heightNumber = 20;
      return css\`
        .foo {
          width: \${width};
          height: \${heightNumber}px;
        }
      \`;
    }
  `;

  const CSS_TEMPLATE_EXPRESSION_SOURCE_MIN = `
    function getSomeCss(){
      const width = '20px';
      const heightNumber = 20;
      return css\`.foo{width:\${width};height:\${heightNumber}px}\`;
    }
  `;

  const CSS_IN_STYLE_TAG_SOURCE = `
    function renderWithStyle() {
      return html\`
        <style>
          .container {
            display: flex;
            padding: 16px;
          }
          
          .item {
            color: blue;
            font-size: 14px;
          }
        </style>
      \`;
    }
  `;

  const CSS_IN_STYLE_TAG_SOURCE_MIN = `
    function renderWithStyle() {
      return html\`<style>.container{display:flex;padding:16px}.item{color:#00f;font-size:14px}</style>\`;
    }
  `;

  const CSS_TEMPLATE_LITERAL_SOURCE = `
    function renderCSSLiteral() {
      return css\`
        .container {
          display: flex;
          padding: 16px;
        }
        
        .item {
          color: blue;
          font-size: 14px;
        }
      \`;
    }
  `;

  const CSS_TEMPLATE_LITERAL_SOURCE_MIN = `
    function renderCSSLiteral() {
      return css\`.container{padding:16px;display:flex}.item{color:#00f;font-size:14px}\`;
    }
  `;

  const CSS_IN_STYLE_TAG_WITH_EXPRESSIONS_SOURCE = `
    function renderWithStyleAndExpressions(color, size) {
      return html\`
        <style>
          .container {
            display: flex;
            padding: 16px;
            background-color: \${color};
          }
          
          .item {
            color: blue;
            font-size: \${size}px;
          }
        </style>
      \`;
    }
  `;

  const CSS_IN_STYLE_TAG_WITH_EXPRESSIONS_SOURCE_MIN = `
    function renderWithStyleAndExpressions(color, size) {
      return html\`<style>.container{display:flex;padding:16px;background-color:\${color}}.item{color:#00f;font-size:\${size}px}</style>\`;
    }
  `;
  const CSS_NESTING_SOURCE = `
    function cssNesting() {
      const spacing = '16px';
      const breakpoint = 768;
      return css\`
        main {
          display: flex;
          padding: \${spacing};
          container-type: inline-size;
          
          section {
            color: blue;
            font-size: 14px;
            margin-bottom: \${spacing};
            
            h1 {
              font-weight: bold;
              line-height: 1.5;
              padding: 8px \${spacing};
            }
            
            .highlight {
              background-color: yellow;
            }
          }
          
          article {
            background-color: white;
            
            @container (min-width: 768px) {
              display: grid;
              grid-template-columns: repeat(3, 1fr);
              gap: 24px;
              
              & .card {
                padding: 12px;
                border-radius: 4px;
                margin-top: \${breakpoint}px;
              }
            }
          }
      
          &:hover {
            background-color: lightgray;
          }
        }
        my-custom-element {
          &:state(custom-state){
              color: green;
          }
        }
      \`;
    }
  `;

  const CSS_NESTING_SOURCE_MIN = `
    function cssNesting() {
      const spacing = '16px';
      const breakpoint = 768;
      return css\`main{padding:\${spacing};display:flex;container-type:inline-size;& section{color:#00f;margin-bottom:\${spacing};font-size:14px;& h1{padding:8px \${spacing}font-weight:700;line-height:1.5}& .highlight{background-color:#ff0}}& article{background-color:#fff;@container (width>=768px){grid-template-columns:repeat(3,1fr);gap:24px;display:grid;& .card{margin-top:\${breakpoint}px;border-radius:4px;padding:12px}}}&:hover{background-color:#d3d3d3}}my-custom-element{&:state(custom-state){color:green}}\`;
    }
  `;
  test('should minify "html" and "css" tagged templates', async () => {
    const result = await minifyHTMLLiterals(SOURCE, {fileName: 'test.js'});
    assert.equal(typeof result, 'object');
    assert.equal(result!.code, SOURCE_MIN);
  });

  test('should minify "svg" tagged templates', async () => {
    const result = await minifyHTMLLiterals(SVG_SOURCE, {fileName: 'test.js'});
    assert.equal(typeof result, 'object');
    assert.equal(result!.code, SVG_SOURCE_MIN);
  });

  test('should minify html with attribute placeholders that have no quotes and JS comments', async () => {
    const result = await minifyHTMLLiterals(COMMENT_SOURCE, {
      fileName: 'test.js',
    });
    assert.equal(typeof result, 'object');
    assert.equal(result!.code, COMMENT_SOURCE_MIN);
  });

  test('should minify html tagged with a member expression ending in html', async () => {
    const result = await minifyHTMLLiterals(MEMBER_EXPRESSION_LITERAL_SOURCE, {
      fileName: 'test.js',
    });
    assert.equal(typeof result, 'object');
    assert.equal(result!.code, MEMBER_EXPRESSION_LITERAL_SOURCE_MIN);
  });

  test('should preserve template expressions, semicolons, and strings after template expressions', async () => {
    const result = await minifyHTMLLiterals(CSS_TEMPLATE_EXPRESSION_SOURCE, {
      fileName: 'test.js',
    });
    assert.equal(typeof result, 'object');
    assert.equal(result!.code, CSS_TEMPLATE_EXPRESSION_SOURCE_MIN);
  });

  test('should minify @container queries and modernize to range syntax', async () => {
    const result = await minifyHTMLLiterals(CONTAINER_QUERY_SOURCE, {
      fileName: 'test.js',
    });
    assert.equal(typeof result, 'object');
    assert.equal(result!.code, CONTAINER_QUERY_SOURCE_MIN);
  });

  test('should minify CSS in style tags the same as CSS template literals', async () => {
    const styleTagResult = await minifyHTMLLiterals(CSS_IN_STYLE_TAG_SOURCE, {
      fileName: 'test.js',
    });
    const cssLiteralResult = await minifyHTMLLiterals(
      CSS_TEMPLATE_LITERAL_SOURCE,
      {
        fileName: 'test.js',
      }
    );

    assert.equal(typeof styleTagResult, 'object');
    assert.equal(styleTagResult!.code, CSS_IN_STYLE_TAG_SOURCE_MIN);

    assert.equal(typeof cssLiteralResult, 'object');
    assert.equal(cssLiteralResult!.code, CSS_TEMPLATE_LITERAL_SOURCE_MIN);

    // Both should produce same minification results
    assert.ok(CSS_IN_STYLE_TAG_SOURCE_MIN.includes('color:#00f'));
    assert.ok(CSS_TEMPLATE_LITERAL_SOURCE_MIN.includes('color:#00f'));
  });

  test('should preserve template expressions in CSS within style tags', async () => {
    const result = await minifyHTMLLiterals(
      CSS_IN_STYLE_TAG_WITH_EXPRESSIONS_SOURCE,
      {
        fileName: 'test.js',
      }
    );

    assert.equal(typeof result, 'object');
    assert.equal(result!.code, CSS_IN_STYLE_TAG_WITH_EXPRESSIONS_SOURCE_MIN);

    // Should preserve template expressions in style tags
    assert.ok(result!.code.includes('background-color:${color}'));
    assert.ok(result!.code.includes('font-size:${size}'));
  });

  test('should minify CSS with native nesting, template expressions, and container queries', async () => {
    const result = await minifyHTMLLiterals(CSS_NESTING_SOURCE, {
      fileName: 'test.js',
    });
    assert.equal(typeof result, 'object');
    assert.equal(result!.code, CSS_NESTING_SOURCE_MIN);

    // Should preserve nesting with semantic HTML elements
    assert.ok(result!.code.includes('& section{'));
    assert.ok(result!.code.includes('& h1{'));
    assert.ok(result!.code.includes('& article{'));

    // Should preserve template expressions in nested context
    assert.ok(result!.code.includes('8px ${spacing}font-weight'));
    assert.ok(result!.code.includes('margin-top:${breakpoint}px'));

    // Should modernize container query to range syntax in nested context
    assert.ok(result!.code.includes('@container (width>=768px)'));
  });

  test('should minify multiline svg elements', async () => {
    const result = await minifyHTMLLiterals(SVG_MULTILINE_SOURCE, {
      fileName: 'test.js',
    });
    assert.equal(typeof result, 'object');
    assert.equal(result!.code, SVG_MULTILINE_SOURCE_MIN);
  });

  test('should not remove spaces in ::part()', async () => {
    const result = await minifyHTMLLiterals(SHADOW_PARTS_SOURCE, {
      fileName: 'test.js',
    });
    assert.equal(typeof result, 'object');
    assert.equal(result!.code, SHADOW_PARTS_SOURCE_MIN);
  });

  test('should return null if source is already minified', async () => {
    const result = await minifyHTMLLiterals(SOURCE_MIN, {fileName: 'test.js'});
    assert.equal(result, null);
  });

  test('should return a v3 source map', async () => {
    const result = await minifyHTMLLiterals(SOURCE, {fileName: 'test.js'});
    assert.equal(typeof result, 'object');
    assert.equal(typeof result!.map, 'object');
    assert.equal(result!.map!.version, 3);
    assert.equal(typeof result!.map!.mappings, 'string');
  });

  // TODO: fix this case
  test('fails to minify static html templates', async () => {
    await assert.rejects(
      async () => await minifyHTMLLiterals(STATIC_SOURCE, {fileName: 'test.js'})
    );
  });

  suite('options', () => {
    let minifyHTMLSpy: Sinon.SinonSpy;

    beforeEach(() => {
      minifyHTMLSpy = Sinon.spy(defaultStrategy, 'minifyHTML');
    });

    afterEach(() => {
      minifyHTMLSpy.restore();
    });

    test('should use defaultMinifyOptions', async () => {
      await minifyHTMLLiterals(SOURCE, {fileName: 'test.js'});
      const parts = parseLiterals(SOURCE)[1].parts;
      const html = defaultStrategy.combineHTMLStrings(
        parts,
        defaultStrategy.getPlaceholder(parts)
      );
      assert.ok(
        minifyHTMLSpy.lastCall.calledWithExactly(html, defaultMinifyOptions)
      );
    });

    test('should allow custom partial minifyOptions', async () => {
      const minifyOptions = {caseSensitive: false};
      await minifyHTMLLiterals(SOURCE, {fileName: 'test.js', minifyOptions});
      const parts = parseLiterals(SOURCE)[1].parts;
      const html = defaultStrategy.combineHTMLStrings(
        parts,
        defaultStrategy.getPlaceholder(parts)
      );
      assert.ok(
        minifyHTMLSpy.lastCall.calledWithExactly(html, {
          ...defaultMinifyOptions,
          ...minifyOptions,
        })
      );
    });

    test('should use MagicString constructor', async () => {
      let msUsed: unknown;
      await minifyHTMLLiterals(SOURCE, {
        fileName: 'test.js',
        generateSourceMap(ms) {
          msUsed = ms;
          return undefined;
        },
      });

      assert.ok(msUsed instanceof MagicString);
    });

    test('should allow custom MagicStringLike constructor', async () => {
      let msUsed: unknown;
      await minifyHTMLLiterals(SOURCE, {
        fileName: 'test.js',
        MagicString: MagicStringLike,
        generateSourceMap(ms) {
          msUsed = ms;
          return undefined;
        },
      });

      assert.ok(msUsed instanceof MagicStringLike);
    });

    test('should allow custom parseLiterals()', async () => {
      const customParseLiterals = Sinon.spy(
        (source: string, options?: ParseLiteralsOptions) => {
          return parseLiterals(source, options);
        }
      );

      await minifyHTMLLiterals(SOURCE, {
        fileName: 'test.js',
        parseLiterals: customParseLiterals,
      });
      assert.ok(customParseLiterals.called);
    });

    test('should allow custom shouldMinify()', async () => {
      const customShouldMinify = Sinon.spy((template: Template) => {
        return defaultShouldMinify(template);
      });

      await minifyHTMLLiterals(SOURCE, {
        fileName: 'test.js',
        shouldMinify: customShouldMinify,
      });
      assert.ok(customShouldMinify.called);
    });

    test('should allow custom strategy', async () => {
      const customStrategy = {
        getPlaceholder: Sinon.spy((parts: TemplatePart[]) => {
          return defaultStrategy.getPlaceholder(parts);
        }),
        combineHTMLStrings: Sinon.spy(
          (parts: TemplatePart[], placeholder: string) => {
            return defaultStrategy.combineHTMLStrings(parts, placeholder);
          }
        ),
        minifyHTML: Sinon.spy((html: string, options?: MinifierOptions) => {
          return defaultStrategy.minifyHTML(html, options);
        }),
        splitHTMLByPlaceholder: Sinon.spy(
          (html: string, placeholder: string) => {
            return defaultStrategy.splitHTMLByPlaceholder(html, placeholder);
          }
        ),
      };

      await minifyHTMLLiterals(SOURCE, {
        fileName: 'test.js',
        strategy: customStrategy,
      });
      assert.ok(customStrategy.getPlaceholder.called);
      assert.ok(customStrategy.combineHTMLStrings.called);
      assert.ok(customStrategy.minifyHTML.called);
      assert.ok(customStrategy.splitHTMLByPlaceholder.called);
    });

    test('should use defaultValidation', async () => {
      await assert.rejects(async () => {
        await minifyHTMLLiterals(SOURCE, {
          fileName: 'test.js',
          strategy: {
            getPlaceholder: () => {
              return ''; // cause an error
            },
            combineHTMLStrings: defaultStrategy.combineHTMLStrings,
            minifyHTML: defaultStrategy.minifyHTML,
            splitHTMLByPlaceholder: defaultStrategy.splitHTMLByPlaceholder,
          },
        });
      });

      await assert.rejects(async () => {
        await minifyHTMLLiterals(SOURCE, {
          fileName: 'test.js',
          strategy: {
            getPlaceholder: defaultStrategy.getPlaceholder,
            combineHTMLStrings: defaultStrategy.combineHTMLStrings,
            minifyHTML: defaultStrategy.minifyHTML,
            splitHTMLByPlaceholder: () => {
              return []; // cause an error
            },
          },
        });
      });
    });

    test('should allow disabling validation', async () => {
      await assert.doesNotReject(async () => {
        await minifyHTMLLiterals(SOURCE, {
          fileName: 'test.js',
          strategy: {
            getPlaceholder: () => {
              return ''; // cause an error
            },
            combineHTMLStrings: defaultStrategy.combineHTMLStrings,
            minifyHTML: defaultStrategy.minifyHTML,
            splitHTMLByPlaceholder: defaultStrategy.splitHTMLByPlaceholder,
          },
          validate: false,
        });
      });
    });

    test('should allow custom validation', async () => {
      const customValidation = {
        ensurePlaceholderValid: Sinon.spy((placeholder: unknown) => {
          return defaultValidation.ensurePlaceholderValid(placeholder);
        }),
        ensureHTMLPartsValid: Sinon.spy(
          (parts: TemplatePart[], htmlParts: string[]) => {
            return defaultValidation.ensureHTMLPartsValid(parts, htmlParts);
          }
        ),
      };

      await minifyHTMLLiterals(SOURCE, {
        fileName: 'test.js',
        validate: customValidation,
      });
      assert.ok(customValidation.ensurePlaceholderValid.called);
      assert.ok(customValidation.ensureHTMLPartsValid.called);
    });

    test('should allow disabling generateSourceMap', async () => {
      const result = await minifyHTMLLiterals(SOURCE, {
        fileName: 'test.js',
        generateSourceMap: false,
      });
      assert.equal(typeof result, 'object');
      assert.equal(result!.map, undefined);
    });

    test('should allow custom generateSourceMap()', async () => {
      const customGenerateSourceMap = Sinon.spy(
        (ms: MagicStringLike, fileName: string) => {
          return defaultGenerateSourceMap(ms, fileName);
        }
      );

      await minifyHTMLLiterals(SOURCE, {
        fileName: 'test.js',
        generateSourceMap: customGenerateSourceMap,
      });
      assert.ok(customGenerateSourceMap.called);
    });
  });

  suite('defaultGenerateSourceMap()', () => {
    test('should call generateMap() on MagicStringLike with .map file, source name, and hires', () => {
      const ms = new MagicStringLike();
      const generateMapSpy = Sinon.spy(ms, 'generateMap');
      defaultGenerateSourceMap(ms, 'test.js');
      assert.ok(
        generateMapSpy.calledWith({
          file: 'test.js.map',
          source: 'test.js',
          hires: true,
        })
      );
    });
  });

  suite('defaultShouldMinify()', () => {
    test('should return true if the template is tagged with any "html" text', () => {
      assert.ok(defaultShouldMinify({tag: 'html', parts: []}));
      assert.ok(defaultShouldMinify({tag: 'HTML', parts: []}));
      assert.ok(defaultShouldMinify({tag: 'hTML', parts: []}));
      assert.ok(defaultShouldMinify({tag: 'getHTML()', parts: []}));
      assert.ok(defaultShouldMinify({tag: 'templateHtml()', parts: []}));
    });

    test('should return false if the template is not tagged or does not contain "html"', () => {
      assert.equal(defaultShouldMinify({parts: []}), false);
      assert.equal(defaultShouldMinify({tag: 'css', parts: []}), false);
    });

    test('should return true if the template is tagged with any "svg" text', () => {
      assert.ok(defaultShouldMinify({tag: 'svg', parts: []}));
      assert.ok(defaultShouldMinify({tag: 'SVG', parts: []}));
      assert.ok(defaultShouldMinify({tag: 'sVg', parts: []}));
      assert.ok(defaultShouldMinify({tag: 'getSVG()', parts: []}));
      assert.ok(defaultShouldMinify({tag: 'templateSvg()', parts: []}));
    });
  });

  suite('defaultShouldMinifyCSS()', () => {
    test('should return true if the template is tagged with any "css" text', () => {
      assert.ok(defaultShouldMinifyCSS({tag: 'css', parts: []}));
      assert.ok(defaultShouldMinifyCSS({tag: 'CSS', parts: []}));
      assert.ok(defaultShouldMinifyCSS({tag: 'csS', parts: []}));
      assert.ok(defaultShouldMinifyCSS({tag: 'getCSS()', parts: []}));
      assert.ok(defaultShouldMinifyCSS({tag: 'templateCss()', parts: []}));
    });

    test('should return false if the template is not tagged or does not contain "css"', () => {
      assert.equal(defaultShouldMinifyCSS({parts: []}), false);
      assert.equal(defaultShouldMinifyCSS({tag: 'html', parts: []}), false);
    });
  });

  suite('defaultValidation', () => {
    suite('ensurePlaceholderValid()', () => {
      test('should throw an error if the placeholder is not a string', () => {
        assert.throws(() => {
          defaultValidation.ensurePlaceholderValid(undefined);
        });
        assert.throws(() => {
          defaultValidation.ensurePlaceholderValid(true);
        });
        assert.throws(() => {
          defaultValidation.ensurePlaceholderValid({});
        });
      });

      test('should throw an error if the placeholder is an empty string', () => {
        assert.throws(() => {
          defaultValidation.ensurePlaceholderValid('');
        });
      });

      test('should not throw an error if the placeholder is a non-empty string', () => {
        assert.doesNotThrow(() => {
          defaultValidation.ensurePlaceholderValid('EXP');
        });
      });
    });
  });
});
