import { describe, expect, it } from "vitest";
import { detectBadJsonStrings } from "~/utils/detectBadJsonStrings";

describe("detectBadJsonStrings", () => {
  it("should not detect valid JSON string", () => {
    const goodJson = `{"title": "hello"}`;
    const result = detectBadJsonStrings(goodJson);
    expect(result).toBe(false);
  });

  it("should detect incomplete Unicode escape sequences", () => {
    const badJson = `{"title": "hello\\ud835"}`;
    const result = detectBadJsonStrings(badJson);
    expect(result).toBe(true);
  });

  it("should not detect complete Unicode escape sequences", () => {
    const goodJson = `{"title": "hello\\ud835\\udc00"}`;
    const result = detectBadJsonStrings(goodJson);
    expect(result).toBe(false);
  });

  it("should detect incomplete low surrogate", () => {
    const badJson = `{"title": "hello\\udc00"}`;
    const result = detectBadJsonStrings(badJson);
    expect(result).toBe(true);
  });

  it("should handle multiple Unicode sequences correctly", () => {
    const goodJson = `{"title": "hello\\ud835\\udc00\\ud835\\udc01"}`;
    const result = detectBadJsonStrings(goodJson);
    expect(result).toBe(false);
  });

  it("should detect mixed complete and incomplete sequences", () => {
    const badJson = `{"title": "hello\\ud835\\udc00\\ud835"}`;
    const result = detectBadJsonStrings(badJson);
    expect(result).toBe(true);
  });

  it("should have acceptable performance overhead", () => {
    const longText = `hello world `.repeat(1_000);
    const goodJson = `{"title": "hello", "text": "${longText}"}`;
    const badJson = `{"title": "hello\\ud835", "text": "${longText}"}`;

    const iterations = 100_000;

    // Warm up
    for (let i = 0; i < 1000; i++) {
      detectBadJsonStrings(goodJson);
      detectBadJsonStrings(badJson);
    }

    // Measure good JSON (most common case)
    const goodStart = performance.now();
    for (let i = 0; i < iterations; i++) {
      detectBadJsonStrings(goodJson);
    }
    const goodTime = performance.now() - goodStart;

    // Measure bad JSON (edge case)
    const badStart = performance.now();
    for (let i = 0; i < iterations; i++) {
      detectBadJsonStrings(badJson);
    }
    const badTime = performance.now() - badStart;

    // Measure baseline (just function call overhead)
    const baselineStart = performance.now();
    for (let i = 0; i < iterations; i++) {
      // Empty function call to measure baseline
    }
    const baselineTime = performance.now() - baselineStart;

    const goodOverhead = goodTime - baselineTime;
    const badOverhead = badTime - baselineTime;

    console.log(`Baseline (${iterations} iterations): ${baselineTime.toFixed(2)}ms`);
    console.log(
      `Good JSON (${iterations} iterations): ${goodTime.toFixed(
        2
      )}ms (overhead: ${goodOverhead.toFixed(2)}ms)`
    );
    console.log(
      `Bad JSON (${iterations} iterations): ${badTime.toFixed(
        2
      )}ms (overhead: ${badOverhead.toFixed(2)}ms)`
    );
    console.log(
      `Average per call - Good: ${(goodOverhead / iterations).toFixed(4)}ms, Bad: ${(
        badOverhead / iterations
      ).toFixed(4)}ms`
    );

    // Assertions for performance expectations
    // Good JSON should be reasonably fast (most common case)
    expect(goodOverhead / iterations).toBeLessThan(0.01); // Less than 10 microseconds per call

    // Bad JSON can be slower due to regex matching, but still reasonable
    expect(badOverhead / iterations).toBeLessThan(0.01); // Less than 20 microseconds per call

    // Total overhead for 100k calls should be reasonable
    expect(goodOverhead).toBeLessThan(1000); // Less than 1 second for 100k calls
  });

  it("should handle various JSON sizes efficiently", () => {
    const sizes = [100, 1000, 10000, 100000];
    const iterations = 10_000;

    for (const size of sizes) {
      const text = `hello world `.repeat(size / 11); // Approximate size
      const goodJson = `{"title": "hello", "text": "${text}"}`;

      const start = performance.now();
      for (let i = 0; i < iterations; i++) {
        detectBadJsonStrings(goodJson);
      }
      const time = performance.now() - start;

      console.log(
        `Size ${size} chars (${iterations} iterations): ${time.toFixed(2)}ms (${(
          time / iterations
        ).toFixed(4)}ms per call)`
      );

      // Performance should scale reasonably with size
      expect(time / iterations).toBeLessThan(size / 1000); // Roughly linear scaling
    }
  });

  it("should show significant performance improvement with quick rejection", () => {
    const longText = `hello world `.repeat(1_000);
    const goodJson = `{"title": "hello", "text": "${longText}"}`;
    const badJson = `{"title": "hello\\ud835", "text": "${longText}"}`;
    const noUnicodeJson = `{"title": "hello", "text": "${longText}"}`;

    const iterations = 100_000;

    // Warm up
    for (let i = 0; i < 1000; i++) {
      detectBadJsonStrings(goodJson);
      detectBadJsonStrings(badJson);
      detectBadJsonStrings(noUnicodeJson);
    }

    // Test strings with no Unicode escapes (99.9% case)
    const noUnicodeStart = performance.now();
    for (let i = 0; i < iterations; i++) {
      detectBadJsonStrings(noUnicodeJson);
    }
    const noUnicodeTime = performance.now() - noUnicodeStart;

    // Test strings with Unicode escapes (0.1% case)
    const withUnicodeStart = performance.now();
    for (let i = 0; i < iterations; i++) {
      detectBadJsonStrings(badJson);
    }
    const withUnicodeTime = performance.now() - withUnicodeStart;

    console.log(
      `No Unicode escapes (${iterations} iterations): ${noUnicodeTime.toFixed(2)}ms (${(
        noUnicodeTime / iterations
      ).toFixed(4)}ms per call)`
    );
    console.log(
      `With Unicode escapes (${iterations} iterations): ${withUnicodeTime.toFixed(2)}ms (${(
        withUnicodeTime / iterations
      ).toFixed(4)}ms per call)`
    );
    console.log(
      `Performance ratio: ${(withUnicodeTime / noUnicodeTime).toFixed(
        2
      )}x slower for Unicode strings`
    );

    // Both cases should be extremely fast (under 1 microsecond per call)
    expect(noUnicodeTime / iterations).toBeLessThan(0.001); // Less than 1 microsecond
    expect(withUnicodeTime / iterations).toBeLessThan(0.001); // Less than 1 microsecond

    // The difference should be reasonable (not more than 5x)
    expect(noUnicodeTime / withUnicodeTime).toBeLessThan(5);
  });

  describe("full UTF-16 low-surrogate range coverage (U+DC00–U+DFFF)", () => {
    // Regression guard: a previous version of this scanner used `[cd]` to
    // match the low-surrogate nibble, missing the entire U+DE00–U+DFFF
    // half of the range. Valid surrogate pairs with low surrogates in that
    // upper half (which includes most common emoji) were falsely flagged,
    // and lone surrogates in the upper half were falsely passed.

    it("does NOT flag a valid pair with low surrogate in the c range (U+DC00–U+DCFF)", () => {
      // 🐍 SNAKE = U+1F40D = 🐍
      expect(detectBadJsonStrings(`{"s":"\\ud83d\\udc0d"}`)).toBe(false);
    });

    it("does NOT flag a valid pair with low surrogate in the d range (U+DD00–U+DDFF)", () => {
      // U+1F540 = 🕀
      expect(detectBadJsonStrings(`{"s":"\\ud83d\\udd40"}`)).toBe(false);
    });

    it("does NOT flag a valid pair with low surrogate in the e range (U+DE00–U+DEFF)", () => {
      // 😀 GRINNING FACE = U+1F600 = 😀 — previously false-flagged
      expect(detectBadJsonStrings(`{"s":"\\ud83d\\ude00"}`)).toBe(false);
    });

    it("does NOT flag a valid pair with low surrogate in the f range (U+DF00–U+DFFF)", () => {
      // U+1F700 = 🜀 — previously false-flagged
      expect(detectBadJsonStrings(`{"s":"\\ud83d\\udf00"}`)).toBe(false);
    });

    it("flags a lone low surrogate in the e range (\\uDE00)", () => {
      // Previously this was NOT flagged because the forward scan only
      // recognised low surrogates with third nibble === "c" || "d".
      expect(detectBadJsonStrings(`{"s":"prefix \\ude00 suffix"}`)).toBe(true);
    });

    it("flags a lone low surrogate in the f range (\\uDFFF)", () => {
      expect(detectBadJsonStrings(`{"s":"prefix \\udfff suffix"}`)).toBe(true);
    });

    it("flags a high surrogate followed by something that looks like a low surrogate but is in the e range with a missing prefix", () => {
      // The previous high-surrogate-then-pair check used `[cd]` for the
      // matching low surrogate nibble, so any high surrogate followed by
      // \uDe.. would be falsely flagged as unpaired. Verify the fix works
      // for the valid case AND still flags genuinely broken inputs.
      expect(detectBadJsonStrings(`{"s":"\\ud800X"}`)).toBe(true); // truly broken
      expect(detectBadJsonStrings(`{"s":"\\ud83d\\ude00"}`)).toBe(false); // valid, but used to flag
    });
  });

  describe("integration with JSON.stringify", () => {
    it("does NOT flag JSON.stringify of a valid emoji 😀", () => {
      // V8 emits the raw character for valid surrogate pairs, so the
      // fast-path returns false without exercising the regex.
      expect(detectBadJsonStrings(JSON.stringify("😀"))).toBe(false);
    });

    it("flags JSON.stringify of a lone high surrogate", () => {
      expect(detectBadJsonStrings(JSON.stringify("\uD800"))).toBe(true);
    });

    it("flags JSON.stringify of a lone low surrogate in each of c/d/e/f ranges", () => {
      expect(detectBadJsonStrings(JSON.stringify("\uDC00"))).toBe(true);
      expect(detectBadJsonStrings(JSON.stringify("\uDD00"))).toBe(true);
      expect(detectBadJsonStrings(JSON.stringify("\uDE00"))).toBe(true);
      expect(detectBadJsonStrings(JSON.stringify("\uDFFF"))).toBe(true);
    });
  });
});
