import { describe, it, expect } from 'vitest';
import { createRingBuffer } from '../src/session/ring-buffer.js';
import type { RingBuffer } from '../src/types.js';

/**
 * T6 / M2 tests — ring-buffer.ts
 *
 * Invariants under test (ARCHITECTURE §3.4 / types.ts impl anchor):
 *  - capacity measured in REAL UTF-8 bytes (Buffer.byteLength), not string.length;
 *  - over-capacity evicts the WHOLE oldest chunk (append boundary), never splitting a
 *    multi-byte UTF-8 codepoint or an ANSI escape sequence mid-way;
 *  - snapshot() prepends a soft-reset "\x1b[0m" then the current buffer.
 */

const SOFT_RESET = '\x1b[0m';

/** snapshot() minus the soft-reset prefix = the live buffer content. */
function body(rb: RingBuffer): string {
  const snap = rb.snapshot();
  expect(snap.startsWith(SOFT_RESET)).toBe(true);
  return snap.slice(SOFT_RESET.length);
}

describe('createRingBuffer', () => {
  describe('snapshot()', () => {
    it('prepends a soft-reset escape even when empty', () => {
      const rb = createRingBuffer(1024);
      expect(rb.snapshot()).toBe(SOFT_RESET);
    });

    it('prepends soft-reset then the current buffer', () => {
      const rb = createRingBuffer(1024);
      rb.append('hello');
      expect(rb.snapshot()).toBe(SOFT_RESET + 'hello');
    });
  });

  describe('tail()', () => {
    it('returns empty for an empty buffer or non-positive maxBytes', () => {
      const rb = createRingBuffer(1024);
      expect(rb.tail(100)).toBe('');
      rb.append('hello');
      expect(rb.tail(0)).toBe('');
    });

    it('returns the most recent chunks up to ~maxBytes, on chunk boundaries', () => {
      const rb = createRingBuffer(1024);
      rb.append('aaaa'); // 4 bytes
      rb.append('bbbb'); // 4 bytes
      rb.append('cccc'); // 4 bytes
      // maxBytes=5 → newest 'cccc' (4) then 'bbbb' (8 ≥ 5) → stop; never splits.
      expect(rb.tail(5)).toBe('bbbbcccc');
    });

    it('returns the whole buffer when maxBytes exceeds its size (no soft-reset)', () => {
      const rb = createRingBuffer(1024);
      rb.append('one');
      rb.append('two');
      expect(rb.tail(1000)).toBe('onetwo');
    });
  });

  describe('within capacity', () => {
    it('replays everything appended, in order', () => {
      const rb = createRingBuffer(1024);
      rb.append('a');
      rb.append('b');
      rb.append('c');
      expect(body(rb)).toBe('abc');
    });

    it('keeps full content when total exactly equals capacity', () => {
      const rb = createRingBuffer(6); // "abcdef" = 6 bytes
      rb.append('abc');
      rb.append('def');
      expect(body(rb)).toBe('abcdef');
    });
  });

  describe('over capacity — evict oldest whole chunk', () => {
    it('drops the oldest chunk(s) when capacity is exceeded', () => {
      const rb = createRingBuffer(6);
      rb.append('aaa'); // 3 bytes
      rb.append('bbb'); // 3 bytes -> total 6, fits
      rb.append('ccc'); // 3 bytes -> total 9 > 6, evict oldest 'aaa'
      expect(body(rb)).toBe('bbbccc');
    });

    it('evicts multiple oldest chunks if needed to fit a large new chunk', () => {
      const rb = createRingBuffer(5);
      rb.append('11'); // 2
      rb.append('22'); // 2 -> total 4
      rb.append('33333'); // 5 bytes; must evict '11' and '22' to fit
      expect(body(rb)).toBe('33333');
    });

    it('evicts on chunk boundaries — never a partial chunk', () => {
      const rb = createRingBuffer(4);
      rb.append('xxx'); // 3
      rb.append('yy'); // 2 -> total 5 > 4, evict whole 'xxx', leaving 'yy'
      // 'xxx' is removed entirely, not trimmed to fit exactly 4 bytes.
      expect(body(rb)).toBe('yy');
    });
  });

  describe('multi-byte UTF-8 is never split mid-codepoint', () => {
    it('does not corrupt Chinese characters on eviction', () => {
      // '中' is 3 UTF-8 bytes.
      const oneChar = '中';
      expect(Buffer.byteLength(oneChar, 'utf8')).toBe(3);

      // Capacity holds ~3 of these chars (9 bytes). Append many in separate chunks.
      const rb = createRingBuffer(9);
      for (let i = 0; i < 10; i++) {
        rb.append('中'); // each chunk = one 3-byte codepoint
      }
      const out = body(rb);

      // No replacement char (U+FFFD) => nothing was split into invalid UTF-8.
      expect(out).not.toContain('�');
      // Every char is a clean '中'.
      expect([...out].every((c) => c === '中')).toBe(true);
      // Byte budget respected.
      expect(Buffer.byteLength(out, 'utf8')).toBeLessThanOrEqual(9);
    });

    it('round-trips through Buffer without producing replacement chars', () => {
      const rb = createRingBuffer(12); // 4 chars * 3 bytes
      const chars = ['日', '本', '語', '中', '文', '字'];
      for (const c of chars) rb.append(c);
      const out = body(rb);
      // Re-encode/decode: if any codepoint were split, we'd see U+FFFD.
      const reDecoded = Buffer.from(out, 'utf8').toString('utf8');
      expect(reDecoded).toBe(out);
      expect(out).not.toContain('�');
    });
  });

  describe('ANSI escape sequences are never cut mid-sequence', () => {
    it('keeps escape sequences whole when each is its own chunk', () => {
      const rb = createRingBuffer(20);
      // Each chunk is a complete colored token.
      rb.append('\x1b[1;32mA\x1b[0m'); // green A + reset
      rb.append('\x1b[1;31mB\x1b[0m'); // red B + reset
      rb.append('\x1b[1;34mC\x1b[0m'); // blue C + reset
      const out = body(rb);

      // The oldest chunk(s) may be evicted, but whichever remain are intact:
      // every ESC '[' begins a complete sequence ending in 'm'.
      const escIndexes = [...out.matchAll(/\x1b/g)].map((m) => m.index!);
      for (const idx of escIndexes) {
        const seq = out.slice(idx);
        expect(seq).toMatch(/^\x1b\[[0-9;]*m/);
      }
      // No dangling ESC at the very end (would mean a cut sequence).
      expect(out.endsWith('\x1b')).toBe(false);
    });

    it('does not slice a single ANSI token in half on eviction', () => {
      const rb = createRingBuffer(8);
      const token = '\x1b[1;32m'; // 7 bytes, a complete SGR sequence
      rb.append(token);
      rb.append(token); // total 14 > 8 -> evict whole first token, keep second intact
      const out = body(rb);
      expect(out).toBe(token); // exactly one whole token, not a fragment
      expect(Buffer.byteLength(out, 'utf8')).toBeLessThanOrEqual(8);
    });
  });

  describe('byte accounting (not string.length)', () => {
    it('uses real UTF-8 byte count for capacity, not UTF-16 code units', () => {
      // '😀' is 2 UTF-16 code units (string.length === 2) but 4 UTF-8 bytes.
      const emoji = '😀';
      expect(emoji.length).toBe(2);
      expect(Buffer.byteLength(emoji, 'utf8')).toBe(4);

      // Capacity 8 bytes = exactly two emoji.
      const rb = createRingBuffer(8);
      rb.append(emoji); // 4 bytes
      rb.append(emoji); // 8 bytes total, fits
      rb.append(emoji); // 12 > 8 -> evict oldest, leaving two emoji (8 bytes)
      const out = body(rb);

      expect(Buffer.byteLength(out, 'utf8')).toBe(8);
      expect([...out].length).toBe(2);
      expect(out).not.toContain('�');
    });

    it('counts multi-byte content by bytes when deciding eviction', () => {
      // If accounting used string.length, '中中' (length 2) would seem to fit a
      // capacity-of-3 buffer; by bytes it's 6 and must not both stay.
      const rb = createRingBuffer(3);
      rb.append('中'); // 3 bytes, fits exactly
      rb.append('中'); // 6 > 3 -> evict the first, keep the second
      const out = body(rb);
      expect(out).toBe('中');
      expect(Buffer.byteLength(out, 'utf8')).toBe(3);
    });
  });

  describe('edge cases', () => {
    it('ignores empty appends', () => {
      const rb = createRingBuffer(8);
      rb.append('');
      rb.append('ab');
      rb.append('');
      expect(body(rb)).toBe('ab');
    });

    it('a single chunk larger than capacity is kept as the sole chunk (cannot split)', () => {
      // Eviction is on whole-chunk boundaries; a lone oversized chunk has nothing
      // older to drop, so it stays intact rather than being sliced (M2: never split).
      const rb = createRingBuffer(4);
      rb.append('abcdefgh'); // 8 bytes > 4
      expect(body(rb)).toBe('abcdefgh');
    });
  });
});
