/**
 * @file Tests that TranscriptCache caps the size of each per-entry array
 * (turnDurations / errors / compaction.entries / usageExtras.*) so a long
 * session cannot grow a single cache entry without bound.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const { describe, it, before, after } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("fs");
const path = require("path");
const os = require("os");

const TranscriptCache = require("../lib/transcript-cache");

let tmpDir;
before(() => {
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tc-bounded-"));
});
after(() => {
  fs.rmSync(tmpDir, { recursive: true, force: true });
});

function writeJsonl(name, lines) {
  const p = path.join(tmpDir, name);
  fs.writeFileSync(p, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
  return p;
}

describe("TranscriptCache._trimArray", () => {
  it("exists and trims arrays to the given max length, keeping the tail", () => {
    const cache = new TranscriptCache();
    assert.equal(typeof cache._trimArray, "function");
    const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    cache._trimArray(arr, 3);
    assert.deepEqual(arr, [8, 9, 10]);
  });

  it("is a no-op when array is within the cap", () => {
    const cache = new TranscriptCache();
    const arr = [1, 2, 3];
    cache._trimArray(arr, 5);
    assert.deepEqual(arr, [1, 2, 3]);
  });

  it("handles null/undefined safely", () => {
    const cache = new TranscriptCache();
    assert.doesNotThrow(() => cache._trimArray(null, 5));
    assert.doesNotThrow(() => cache._trimArray(undefined, 5));
  });
});

describe("TranscriptCache.extract — array caps", () => {
  it("caps turnDurations at MAX_ARRAY_LEN on full read, keeping the tail", () => {
    // 1500 turn_duration entries, ascending timestamps
    const lines = [];
    for (let i = 0; i < 1500; i++) {
      lines.push({
        type: "system",
        subtype: "turn_duration",
        durationMs: i + 1,
        timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
      });
    }
    const p = writeJsonl("turns.jsonl", lines);

    process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN = "100";
    // Re-require fresh to pick up the env override
    delete require.cache[require.resolve("../lib/transcript-cache")];
    const Fresh = require("../lib/transcript-cache");
    const cache = new Fresh();

    const result = cache.extract(p);
    assert.ok(result, "expected non-null result");
    assert.equal(result.turnDurations.length, 100);
    // Tail-kept: durationMs should be 1401..1500
    assert.equal(result.turnDurations[0].durationMs, 1401);
    assert.equal(result.turnDurations[99].durationMs, 1500);

    delete process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN;
    delete require.cache[require.resolve("../lib/transcript-cache")];
  });

  it("caps errors and compaction.entries on full read", () => {
    const lines = [];
    for (let i = 0; i < 300; i++) {
      lines.push({
        isApiErrorMessage: true,
        error: "rate_limit",
        message: { content: [{ text: `err-${i}` }] },
        timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
      });
      lines.push({
        isCompactSummary: true,
        uuid: `c-${i}`,
        timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
      });
    }
    const p = writeJsonl("err-compact.jsonl", lines);

    process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN = "50";
    delete require.cache[require.resolve("../lib/transcript-cache")];
    const Fresh = require("../lib/transcript-cache");
    const cache = new Fresh();
    const result = cache.extract(p);

    assert.equal(result.errors.length, 50);
    assert.equal(result.compaction.entries.length, 50);
    assert.equal(
      result.compaction.count,
      300,
      "count must reflect ALL parsed entries, not just retained"
    );

    delete process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN;
    delete require.cache[require.resolve("../lib/transcript-cache")];
  });

  it("incremental merge respects cap (append to existing capped entry)", () => {
    process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN = "100";
    delete require.cache[require.resolve("../lib/transcript-cache")];
    const Fresh = require("../lib/transcript-cache");
    const cache = new Fresh();

    // First batch: 80 turns
    const linesA = [];
    for (let i = 0; i < 80; i++) {
      linesA.push({
        type: "system",
        subtype: "turn_duration",
        durationMs: i + 1,
        timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
      });
    }
    const p = writeJsonl("incr.jsonl", linesA);
    let result = cache.extract(p);
    assert.equal(result.turnDurations.length, 80);

    // Append 50 more — total 130, cache should retain only last 100
    const fd = fs.openSync(p, "a");
    for (let i = 80; i < 130; i++) {
      const line =
        JSON.stringify({
          type: "system",
          subtype: "turn_duration",
          durationMs: i + 1,
          timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
        }) + "\n";
      fs.writeSync(fd, line);
    }
    fs.closeSync(fd);

    result = cache.extract(p);
    assert.equal(result.turnDurations.length, 100);
    // Tail check: last entry should be durationMs=130
    assert.equal(result.turnDurations[99].durationMs, 130);
    // Head should be durationMs=31 (130 - 100 + 1)
    assert.equal(result.turnDurations[0].durationMs, 31);

    delete process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN;
    delete require.cache[require.resolve("../lib/transcript-cache")];
  });
});

describe("TranscriptCache._set — single storage", () => {
  it("cache entry contains ONLY {mtimeMs, size, bytesRead, result}", () => {
    const p = writeJsonl("single.jsonl", [
      {
        type: "system",
        subtype: "turn_duration",
        durationMs: 100,
        timestamp: "2026-01-01T00:00:00Z",
      },
    ]);
    const cache = new TranscriptCache();
    cache.extract(p);
    const entry = cache._cache.get(p);
    assert.ok(entry, "entry should be cached");
    const keys = Object.keys(entry).sort();
    assert.deepEqual(keys, ["bytesRead", "mtimeMs", "result", "size"]);
  });

  it("does not store duplicate top-level errors/turnDurations/compaction", () => {
    const p = writeJsonl("dup.jsonl", [
      {
        type: "system",
        subtype: "turn_duration",
        durationMs: 1,
        timestamp: "2026-01-01T00:00:00Z",
      },
      {
        isApiErrorMessage: true,
        error: "x",
        message: { content: [{ text: "y" }] },
        timestamp: "2026-01-01T00:00:01Z",
      },
      { isCompactSummary: true, uuid: "u1", timestamp: "2026-01-01T00:00:02Z" },
    ]);
    const cache = new TranscriptCache();
    cache.extract(p);
    const entry = cache._cache.get(p);
    assert.equal(entry.errors, undefined);
    assert.equal(entry.turnDurations, undefined);
    assert.equal(entry.compaction, undefined);
    assert.equal(entry.tokensByModel, undefined);
    assert.equal(entry.usageExtras, undefined);
    assert.equal(entry.thinkingBlockCount, undefined);
    assert.equal(entry.latestModel, undefined);
  });
});
