import { describe, test, expect } from 'vitest';
import { formatHtml } from './format-html';

describe('formatHtml', () => {
  test('indents nested block elements two spaces per level', () => {
    expect(formatHtml('<div><section><p>hi</p></section></div>')).toBe(
      '<div>\n  <section>\n    <p>hi</p>\n  </section>\n</div>',
    );
  });

  test('leaves inline tags on the same line as their text', () => {
    expect(formatHtml('<p>hello <strong>world</strong> now</p>')).toBe(
      '<p>hello <strong>world</strong> now</p>',
    );
  });

  test('void elements do not open an indent level', () => {
    expect(formatHtml('<div><img src="a.png"><br><p>x</p></div>')).toBe(
      '<div>\n  <img src="a.png">\n  <br>\n  <p>x</p>\n</div>',
    );
  });

  test('preserves <pre> contents verbatim', () => {
    const src = '<div><pre>  keep\n   this</pre></div>';
    expect(formatHtml(src)).toBe('<div>\n  <pre>  keep\n   this</pre>\n</div>');
  });

  test('is idempotent', () => {
    const once = formatHtml('<div><section><p>hi</p></section></div>');
    expect(formatHtml(once)).toBe(once);
  });

  test('empty and whitespace-only input round-trip to an empty string', () => {
    expect(formatHtml('')).toBe('');
    expect(formatHtml('   \n  ')).toBe('');
  });

  test('unbalanced markup never produces negative indent', () => {
    expect(formatHtml('</div><p>x</p>')).toBe('</div>\n<p>x</p>');
  });
});

// Regression coverage from code review: only <pre> was originally exempted
// from the naive '<'/'>' tag-boundary scan, which let a '>' inside a quoted
// attribute value corrupt output, and let '<'/'>' inside <script>/<style>
// content be misparsed as tag boundaries.
describe('formatHtml - raw content and quoted attributes', () => {
  test('a ">" inside a quoted attribute value does not split the tag', () => {
    const src = '<div title="a>b"><p>hi</p></div><footer>bye</footer>';
    expect(formatHtml(src)).toBe(
      '<div title="a>b">\n  <p>hi</p>\n</div>\n<footer>bye</footer>',
    );
  });

  test('"<" and ">" inside <script> content do not desync sibling nesting', () => {
    const src = '<div><script>a < b;</script></div><div><script>c < d;</script></div>';
    expect(formatHtml(src)).toBe(
      '<div>\n  <script>a < b;</script>\n</div>\n<div>\n  <script>c < d;</script>\n</div>',
    );
  });

  test('a CSS child combinator inside <style> content is not treated as markup', () => {
    const src = '<div><style>div > p { color: red; }</style></div>';
    expect(formatHtml(src)).toBe(
      '<div>\n  <style>div > p { color: red; }</style>\n</div>',
    );
  });

  test('is idempotent across a quoted ">" attribute and <script> content', () => {
    const quotedAttr = formatHtml('<div title="a>b"><p>hi</p></div><footer>bye</footer>');
    expect(formatHtml(quotedAttr)).toBe(quotedAttr);

    const scriptSrc = '<div><script>a < b;</script></div><div><script>c < d;</script></div>';
    const scripted = formatHtml(scriptSrc);
    expect(formatHtml(scripted)).toBe(scripted);
  });

  test('an unclosed <pre> is swallowed verbatim to the end of the document', () => {
    expect(formatHtml('<div><pre>no closing tag here')).toBe(
      '<div>\n  <pre>no closing tag here',
    );
  });
});

// Regression coverage from round 2 of code review: a close tag whose
// innermost open frame doesn't match it must not wedge the stack for the
// rest of the document. An optional end tag skipped by the author (e.g.
// an unclosed <p>) must self-drain against its real ancestor close tag,
// while a close tag with no opener anywhere on the stack still has nothing
// to pair with and is left exactly where it is.
describe('formatHtml - mismatched close recovery', () => {
  test('an unclosed <p> before </div> drains against the ancestor close instead of wedging the stack', () => {
    const src = '<div><p>one<p>two</p></div><p>three</p>';
    expect(formatHtml(src)).toBe(
      '<div>\n  <p>\n    one\n    <p>two</p>\n</div>\n<p>three</p>',
    );
  });

  test('a close tag with no opener anywhere on the stack is left in place', () => {
    const src = '<div><p>x</p></footer></div>';
    expect(formatHtml(src)).toBe('<div>\n  <p>x</p>\n  </footer>\n</div>');
  });

  test('is idempotent across an unclosed <p> and a stray close with no opener', () => {
    const unclosedP = formatHtml('<div><p>one<p>two</p></div><p>three</p>');
    expect(formatHtml(unclosedP)).toBe(unclosedP);

    const strayClose = formatHtml('<div><p>x</p></footer></div>');
    expect(formatHtml(strayClose)).toBe(strayClose);
  });
});

// Regression coverage from round 3 of code review: the previous fix for the
// phantom-space idempotency bug used "does this boundary whitespace contain
// a newline" as its drop/keep rule, which also stripped hand-wrapped text
// like "hello\n<strong>" -- a single-pass content change, not a
// repeated-pass artifact. The correct discriminator is what the whitespace
// borders: whitespace between two inline-level things (text, <strong>, ...)
// is always significant and must survive regardless of newlines; whitespace
// touching a block-tag boundary carries no rendered meaning and is always
// dropped, regardless of newlines.
describe('formatHtml - inline whitespace vs block-boundary whitespace', () => {
  test('a newline between text and an inline tag is kept as a single space', () => {
    expect(formatHtml('<p>hello\n<strong>world</strong></p>')).toBe(
      '<p>hello <strong>world</strong></p>',
    );
  });

  test('a literal space in the same position is kept unchanged (pin)', () => {
    expect(formatHtml('<p>hello <strong>world</strong></p>')).toBe(
      '<p>hello <strong>world</strong></p>',
    );
  });

  test('a newline at a block-tag boundary is still dropped', () => {
    expect(formatHtml('<div>\n</div>')).toBe('<div></div>');
  });

  test('is idempotent across inline-boundary whitespace, block-boundary whitespace, and the case that originally exposed the phantom space', () => {
    const wrapped = formatHtml('<p>hello\n<strong>world</strong></p>');
    expect(formatHtml(wrapped)).toBe(wrapped);

    const sameLine = formatHtml('<p>hello <strong>world</strong></p>');
    expect(formatHtml(sameLine)).toBe(sameLine);

    const blockGap = formatHtml('<div>\n</div>');
    expect(formatHtml(blockGap)).toBe(blockGap);

    const phantomSpaceCase = formatHtml('<div><p>one<p>two</p></div><p>three</p>');
    expect(formatHtml(phantomSpaceCase)).toBe(phantomSpaceCase);
  });
});
