import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { buildDeps, mockMsg, seedMessages } from './helpers/incremental-context-helpers.js';

const { assembleIncrementalContext } = await import('../dist/domains/cats/services/agents/routing/route-helpers.js');
const { MessageStore } = await import('../dist/domains/cats/services/stores/ports/MessageStore.js');
const { DeliveryCursorStore } = await import('../dist/domains/cats/services/stores/ports/DeliveryCursorStore.js');
const { getCatContextBudget } = await import('../dist/config/cat-budgets.js');

describe('assembleIncrementalContext — GAP-1 budget enforcement', () => {
  test('caps messages to maxMessages when cursor is undefined (first-time cat)', async () => {
    const budget = getCatContextBudget('opus');
    const overCount = budget.maxMessages + 50;

    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    const msgs = seedMessages(messageStore, overCount);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus');

    const deliveredCount = (result.contextText.match(/\[(\d{16}-\d{6}-[a-f0-9]{8})\]/g) || []).length;
    assert.ok(
      deliveredCount <= budget.maxMessages,
      `Expected at most ${budget.maxMessages} messages, got ${deliveredCount}`,
    );

    assert.ok(result.contextText.includes(msgs[msgs.length - 1].id), 'Should include the newest message');
    // F148 Phase C: msgs[0] may appear as primacy anchor [Thread opener: {id}].
    // Anchor format does NOT contain `[{id}]` (burst format), so this check is precise.
    const oldestInBurst = result.contextText.includes(`[${msgs[0].id}]`);
    assert.ok(
      !oldestInBurst,
      'Oldest message must not appear in burst format (may appear as [Thread opener: ...] anchor)',
    );
  });

  test('caps messages when stale cursor produces large unseen batch', async () => {
    const budget = getCatContextBudget('opus');
    const totalCount = budget.maxMessages + 100;

    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    const msgs = seedMessages(messageStore, totalCount);

    await deliveryCursorStore.ackCursor('user-1', 'opus', 'thread-1', msgs[9].id);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus');

    const deliveredCount = (result.contextText.match(/\[(\d{16}-\d{6}-[a-f0-9]{8})\]/g) || []).length;
    assert.ok(
      deliveredCount <= budget.maxMessages,
      `Stale cursor: expected at most ${budget.maxMessages} messages, got ${deliveredCount}`,
    );

    assert.ok(result.contextText.includes(msgs[msgs.length - 1].id), 'Should include the newest message');
  });

  test('includesCurrentUserMessage is based on capped set, not raw relevant', async () => {
    const budget = getCatContextBudget('opus');
    const overCount = budget.maxMessages + 50;

    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    const msgs = seedMessages(messageStore, overCount);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const currentMsgId = msgs[msgs.length - 1].id;
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus', currentMsgId);

    assert.equal(result.includesCurrentUserMessage, true, 'Current user message (newest) should be in capped set');
  });

  test('includesCurrentUserMessage is false when current msg is in oldest capped-off portion', async () => {
    const budget = getCatContextBudget('opus');
    const overCount = budget.maxMessages + 50;

    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    const msgs = seedMessages(messageStore, overCount);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const oldMsgId = msgs[0].id;
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus', oldMsgId);

    assert.equal(result.includesCurrentUserMessage, false, 'Old message capped off should not be reported as included');
  });

  test('does NOT truncate when message count is within budget (warm path)', async () => {
    // F148: use ≤15 to stay on warm path
    const withinCount = 10;
    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    seedMessages(messageStore, withinCount);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus');

    const deliveredCount = (result.contextText.match(/\[(\d{16}-\d{6}-[a-f0-9]{8})\]/g) || []).length;
    assert.equal(deliveredCount, withinCount, `All ${withinCount} messages should be delivered without truncation`);
  });

  test('boundaryId is the last message in capped set', async () => {
    const budget = getCatContextBudget('opus');
    const overCount = budget.maxMessages + 50;

    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    const msgs = seedMessages(messageStore, overCount);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus');

    assert.equal(result.boundaryId, msgs[msgs.length - 1].id, 'boundaryId should be the newest message ID');
  });

  test('currentMessageFilteredOut reflects visibility filtering, not budget cap', async () => {
    const budget = getCatContextBudget('opus');
    const overCount = budget.maxMessages + 50;

    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    const msgs = seedMessages(messageStore, overCount);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const oldMsgId = msgs[0].id;
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus', oldMsgId);

    assert.equal(
      result.currentMessageFilteredOut,
      false,
      'Budget cap should NOT set currentMessageFilteredOut (reserved for visibility/whisper filtering)',
    );
  });

  test('returns context when messages exceed budget', async () => {
    const budget = getCatContextBudget('opus');
    const overCount = budget.maxMessages + 50;

    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    seedMessages(messageStore, overCount);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus');

    // With F148: cold path activates (overCount > 15), burst + tombstone handles it.
    // Either warm-path degradation or cold-path smart window is acceptable.
    assert.ok(result.contextText.length > 0, 'Should produce context regardless of path');
  });

  test('no degradation when within budget', async () => {
    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    seedMessages(messageStore, 10);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus');

    assert.ok(!result.degradation, 'Should NOT report degradation when within budget');
  });

  test('context header shows count info after cap', async () => {
    const budget = getCatContextBudget('opus');
    const overCount = budget.maxMessages + 50;

    const messageStore = new MessageStore();
    const deliveryCursorStore = new DeliveryCursorStore();
    seedMessages(messageStore, overCount);

    const deps = buildDeps(messageStore, deliveryCursorStore);
    const result = await assembleIncrementalContext(deps, 'user-1', 'thread-1', 'opus');

    // F148: cold path uses "智能窗口" header, warm path uses "未发送过 N 条" header
    const warmHeader = result.contextText.match(/未发送过 (\d+) 条/);
    const coldHeader = result.contextText.includes('智能窗口');
    assert.ok(warmHeader || coldHeader, 'Context should have warm or cold path header');
  });
});
