'use strict';

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 { createLocalStorage } = require('../server/storage/local');

// ── loadEntryReqRes (content-addressed) ─────────────────────────────

describe('loadEntryReqRes', () => {
  const tmpDir = path.join(os.tmpdir(), 'ccxray-restore-test-' + Date.now());
  let storage;

  before(async () => {
    storage = createLocalStorage(tmpDir);
    await storage.init();
  });

  after(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it('reconstructs full req from stripped _req.json + shared files', async () => {
    const sys = [{ type: 'text', text: 'system prompt' }];
    const tools = [{ name: 'bash', description: 'run bash' }];
    const messages = [{ role: 'user', content: 'hello' }];
    const sysHash = 'aaa111';
    const toolsHash = 'bbb222';

    await storage.writeSharedIfAbsent(`sys_${sysHash}.json`, JSON.stringify(sys));
    await storage.writeSharedIfAbsent(`tools_${toolsHash}.json`, JSON.stringify(tools));
    await storage.write('2026-04-02T08-00-00-000', '_req.json',
      JSON.stringify({ model: 'claude-opus-4-6', max_tokens: 1000, messages, sysHash, toolsHash }));
    await storage.write('2026-04-02T08-00-00-000', '_res.json',
      JSON.stringify([{ type: 'message_start' }]));

    // Import loadEntryReqRes after setting up the storage (it uses config.storage internally)
    // We test the reconstruction logic via the storage primitives
    const stripped = JSON.parse(await storage.read('2026-04-02T08-00-00-000', '_req.json'));
    const loadedSys = JSON.parse(await storage.readShared(`sys_${stripped.sysHash}.json`));
    const loadedTools = JSON.parse(await storage.readShared(`tools_${stripped.toolsHash}.json`));
    const fullReq = { ...stripped, system: loadedSys, tools: loadedTools };
    delete fullReq.sysHash;
    delete fullReq.toolsHash;

    assert.deepEqual(fullReq.system, sys);
    assert.deepEqual(fullReq.tools, tools);
    assert.deepEqual(fullReq.messages, messages);
    assert.equal(fullReq.model, 'claude-opus-4-6');
    assert.ok(!('sysHash' in fullReq));
    assert.ok(!('toolsHash' in fullReq));
  });

  it('handles null sysHash and toolsHash gracefully', async () => {
    await storage.write('2026-04-02T09-00-00-000', '_req.json',
      JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: 100, messages: [], sysHash: null, toolsHash: null }));

    const stripped = JSON.parse(await storage.read('2026-04-02T09-00-00-000', '_req.json'));
    const sys = stripped.sysHash ? JSON.parse(await storage.readShared(`sys_${stripped.sysHash}.json`)) : null;
    const tools = stripped.toolsHash ? JSON.parse(await storage.readShared(`tools_${stripped.toolsHash}.json`)) : null;

    assert.equal(sys, null);
    assert.equal(tools, null);
  });

  it('res stored as minified JSON parses correctly', async () => {
    const events = [{ type: 'message_start', message: { id: 'x' } }, { type: 'message_stop' }];
    await storage.write('2026-04-02T10-00-00-000', '_res.json', JSON.stringify(events));
    const raw = await storage.read('2026-04-02T10-00-00-000', '_res.json');
    // Should not have newlines/spaces (minified)
    assert.ok(!raw.includes('\n'));
    assert.deepEqual(JSON.parse(raw), events);
  });
});

// ── restoreFromLogs (index.ndjson) ───────────────────────────────────

describe('restoreFromLogs', () => {
  it('returns cleanly when no index.ndjson exists', async () => {
    // loadEntryReqRes with a non-existent entry should set _loaded=true and req/res=null
    const { loadEntryReqRes } = require('../server/restore');
    const entry = { id: 'no-such-file', _loaded: false, req: null, res: null };
    await loadEntryReqRes(entry);
    assert.equal(entry._loaded, true);
    assert.equal(entry.req, null);
    assert.equal(entry.res, null);
  });
});

// Restored entries must re-apply usage-aware context inference, otherwise
// historical index lines that captured maxContext=200000 for Claude 1M-plan
// turns (where no system prompt was sent so the [1m] marker couldn't be
// extracted) keep displaying "636K / 200K (clamped to 100%)" forever.
describe('restoreFromLogs — maxContext re-inference for legacy entries', () => {
  const config = require('../server/config');
  const store = require('../server/store');
  const { restoreFromLogs } = require('../server/restore');
  const tmpDir = path.join(os.tmpdir(), 'ccxray-restore-infer-' + Date.now());
  let realStorage;
  let realRestoreDays;

  before(async () => {
    realStorage = config.storage;
    realRestoreDays = config.RESTORE_DAYS;
    // Bypass the RESTORE_DAYS date-window filter. The synthetic entries below
    // use hardcoded 2026-05-14 ids that become "older than the cutoff" as time
    // passes from when this suite was written, and would otherwise be silently
    // dropped from store.entries — making the suite green at write time and
    // red a few days later.
    config.RESTORE_DAYS = 0;
    const tmpStorage = require('../server/storage/local').createLocalStorage(tmpDir);
    await tmpStorage.init();
    config.storage = tmpStorage;
  });

  after(() => {
    config.storage = realStorage;
    config.RESTORE_DAYS = realRestoreDays;
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  it('bumps stored maxContext=200000 to 1M when restored usage exceeds it', async () => {
    store.entries.length = 0;
    const id = '2026-05-14T13-27-40-199';
    // Reproduces the real-world bug pattern: bare model, no [1m] marker
    // captured at write time, but usage clearly exceeds 200K.
    await config.storage.appendIndex(JSON.stringify({
      id, ts: '13:27:40', sessionId: 'sess-1',
      provider: 'anthropic', agent: 'claude',
      model: 'claude-opus-4-7',
      usage: { input_tokens: 632129, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
      maxContext: 200000,
      isSSE: true, status: 200, receivedAt: 1779000000000,
    }) + '\n');

    await restoreFromLogs();
    const entry = store.entries.find(e => e.id === id);
    assert.ok(entry, 'expected restored entry');
    assert.equal(entry.maxContext, 1_000_000);
  });

  it('keeps stored maxContext=1000000 even when current usage is small (no downgrade)', async () => {
    store.entries.length = 0;
    const id = '2026-05-14T14-00-00-000';
    await config.storage.appendIndex(JSON.stringify({
      id, ts: '14:00:00', sessionId: 'sess-2',
      provider: 'anthropic', agent: 'claude',
      model: 'claude-opus-4-7',
      usage: { input_tokens: 50000 },
      maxContext: 1_000_000, // originally detected correctly via [1m] in system
      isSSE: true, status: 200, receivedAt: 1779000000000,
    }) + '\n');

    await restoreFromLogs();
    const entry = store.entries.find(e => e.id === id);
    assert.ok(entry);
    assert.equal(entry.maxContext, 1_000_000);
  });

  it('leaves OpenAI entries untouched (no Claude bump)', async () => {
    store.entries.length = 0;
    const id = '2026-05-14T15-00-00-000';
    await config.storage.appendIndex(JSON.stringify({
      id, ts: '15:00:00', sessionId: 'sess-3',
      provider: 'openai', agent: 'codex',
      model: 'gpt-5',
      usage: { input_tokens: 500000 }, // exceeds 400K base
      maxContext: null,
      isSSE: true, status: 200, receivedAt: 1779000000000,
    }) + '\n');

    await restoreFromLogs();
    const entry = store.entries.find(e => e.id === id);
    assert.ok(entry);
    assert.equal(entry.maxContext, null);
  });
});
