import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import Database from 'better-sqlite3';
import {
  storeTurn,
  storeTurnPair,
  assembleLcmContext,
  storeSummaryNode,
  searchLcm,
  expandSummaryNode,
  describeLcm,
  getAllTurns,
  shouldCompact,
  getTurnsForCompaction,
  deterministicTruncate,
  buildLeafSummaryPrompt,
  runCompaction,
  _initTestLcmDb,
  _injectTestDb,
  _clearDbCache,
} from './lcm.js';

// Enable LCM for all tests
vi.stubEnv('LCM_ENABLED', 'true');

const TEST_GROUP = 'test-tenant/test-group';

beforeEach(() => {
  const db = _initTestLcmDb();
  _injectTestDb(TEST_GROUP, db);
});

afterEach(() => {
  _clearDbCache();
});

describe('schema', () => {
  it('creates all required tables', () => {
    const db = _initTestLcmDb();
    const tables = (db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as Array<{ name: string }>)
      .map(r => r.name)
      .sort();
    expect(tables).toContain('turns');
    expect(tables).toContain('summary_nodes');
    expect(tables).toContain('context_items');
    expect(tables).toContain('lcm_meta');
  });

  it('migrations are idempotent', () => {
    // Running _initTestLcmDb twice should not throw
    const db = _initTestLcmDb();
    _injectTestDb(TEST_GROUP, db);
    // storeTurn triggers runMigrations via getDb — running again is safe
    expect(() => storeTurn({ groupFolder: TEST_GROUP, sessionId: 's1', role: 'user', content: 'hello' })).not.toThrow();
    expect(() => storeTurn({ groupFolder: TEST_GROUP, sessionId: 's1', role: 'user', content: 'hello again' })).not.toThrow();
  });
});

describe('storeTurn', () => {
  it('inserts a user turn', () => {
    storeTurn({ groupFolder: TEST_GROUP, sessionId: 'sess1', role: 'user', content: 'Hello world' });
    const turns = getAllTurns(TEST_GROUP);
    expect(turns).toHaveLength(1);
    expect(turns[0].role).toBe('user');
    expect(turns[0].content).toBe('Hello world');
    expect(turns[0].session_id).toBe('sess1');
  });

  it('inserts an agent turn', () => {
    storeTurn({ groupFolder: TEST_GROUP, sessionId: 'sess1', role: 'agent', content: 'Hi there!' });
    const turns = getAllTurns(TEST_GROUP);
    expect(turns).toHaveLength(1);
    expect(turns[0].role).toBe('agent');
  });

  it('assigns sequential seq numbers', () => {
    storeTurn({ groupFolder: TEST_GROUP, sessionId: 's1', role: 'user', content: 'first' });
    storeTurn({ groupFolder: TEST_GROUP, sessionId: 's1', role: 'agent', content: 'second' });
    storeTurn({ groupFolder: TEST_GROUP, sessionId: 's1', role: 'user', content: 'third' });
    const turns = getAllTurns(TEST_GROUP);
    expect(turns[0].seq).toBe(0);
    expect(turns[1].seq).toBe(1);
    expect(turns[2].seq).toBe(2);
  });

  it('truncates content exceeding 50k chars', () => {
    const longContent = 'x'.repeat(60_000);
    storeTurn({ groupFolder: TEST_GROUP, sessionId: 's1', role: 'user', content: longContent });
    const turns = getAllTurns(TEST_GROUP);
    expect(turns[0].content.length).toBeLessThan(60_000);
    expect(turns[0].content).toContain('[LCM: content truncated for storage]');
  });

  it('stores content under 50k chars unchanged', () => {
    const content = 'a'.repeat(1000);
    storeTurn({ groupFolder: TEST_GROUP, sessionId: 's1', role: 'user', content });
    const turns = getAllTurns(TEST_GROUP);
    expect(turns[0].content).toBe(content);
    expect(turns[0].content).not.toContain('truncated');
  });

  it('stores input_tokens when provided', () => {
    storeTurn({ groupFolder: TEST_GROUP, sessionId: 's1', role: 'agent', content: 'response', inputTokens: 123 });
    const turns = getAllTurns(TEST_GROUP);
    expect(turns[0].input_tokens).toBe(123);
  });
});

describe('storeTurnPair', () => {
  it('inserts both user and agent rows atomically', () => {
    storeTurnPair({
      groupFolder: TEST_GROUP,
      sessionId: 'sess1',
      userContent: 'What is 2+2?',
      agentContent: 'It is 4.',
      inputTokens: 50,
    });
    const turns = getAllTurns(TEST_GROUP);
    expect(turns).toHaveLength(2);
    expect(turns[0].role).toBe('user');
    expect(turns[0].content).toBe('What is 2+2?');
    expect(turns[1].role).toBe('agent');
    expect(turns[1].content).toBe('It is 4.');
    expect(turns[1].input_tokens).toBe(50);
  });

  it('assigns sequential seq numbers for the pair', () => {
    storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: 'u', agentContent: 'a' });
    const turns = getAllTurns(TEST_GROUP);
    expect(turns[0].seq).toBe(0);
    expect(turns[1].seq).toBe(1);
  });
});

describe('assembleLcmContext', () => {
  it('returns empty string for empty DB', () => {
    expect(assembleLcmContext(TEST_GROUP)).toBe('');
  });

  it('returns lcm_context block when turns exist', () => {
    storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: 'my fav number is 42', agentContent: 'Got it!' });
    const ctx = assembleLcmContext(TEST_GROUP);
    expect(ctx).toContain('<lcm_context>');
    expect(ctx).toContain('</lcm_context>');
    expect(ctx).toContain('my fav number is 42');
    expect(ctx).toContain('Got it!');
  });

  it('includes summary nodes when present', () => {
    storeSummaryNode({ groupFolder: TEST_GROUP, depth: 0, content: 'Earlier we discussed project Aurora.', sourceTurnIds: [] });
    storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: 'new message', agentContent: 'ok' });
    const ctx = assembleLcmContext(TEST_GROUP);
    expect(ctx).toContain('project Aurora');
    expect(ctx).toContain('new message');
  });

  it('labels roles correctly', () => {
    storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: 'hello', agentContent: 'hi' });
    const ctx = assembleLcmContext(TEST_GROUP);
    expect(ctx).toContain('**User:**');
    expect(ctx).toContain('**Assistant:**');
  });
});

describe('storeSummaryNode', () => {
  it('inserts a summary node and returns an id', () => {
    const id = storeSummaryNode({
      groupFolder: TEST_GROUP,
      depth: 0,
      content: 'Summary of earlier conversation.',
      sourceTurnIds: ['turn1', 'turn2'],
    });
    expect(typeof id).toBe('string');
    expect(id!.length).toBeGreaterThan(0);
  });
});

describe('searchLcm', () => {
  it('returns empty array when no matches', () => {
    storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: 'hello', agentContent: 'hi' });
    expect(searchLcm(TEST_GROUP, 'xyz-no-match')).toHaveLength(0);
  });

  it('finds matching turn content', () => {
    storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: 'the secret code is banana', agentContent: 'noted' });
    const results = searchLcm(TEST_GROUP, 'banana');
    expect(results.length).toBeGreaterThan(0);
    expect(results[0].excerpt).toContain('banana');
  });

  it('finds matching summary content', () => {
    storeSummaryNode({ groupFolder: TEST_GROUP, depth: 0, content: 'we talked about kumquats', sourceTurnIds: [] });
    const results = searchLcm(TEST_GROUP, 'kumquats');
    expect(results.length).toBeGreaterThan(0);
    expect(results[0].type).toContain('summary');
  });

  it('is case-insensitive', () => {
    storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: 'Project AURORA is active', agentContent: 'ok' });
    expect(searchLcm(TEST_GROUP, 'aurora')).toHaveLength(1);
    expect(searchLcm(TEST_GROUP, 'AURORA')).toHaveLength(1);
  });
});

describe('expandSummaryNode', () => {
  it('returns null for unknown summary id', () => {
    expect(expandSummaryNode(TEST_GROUP, 'nonexistent')).toBeNull();
  });

  it('returns the summary node and its source turns', () => {
    storeTurn({ groupFolder: TEST_GROUP, sessionId: 's1', role: 'user', content: 'turn one' });
    const turns = getAllTurns(TEST_GROUP);
    const turnId = turns[0].id;

    const summaryId = storeSummaryNode({
      groupFolder: TEST_GROUP,
      depth: 0,
      content: 'Summary of turn one.',
      sourceTurnIds: [turnId],
    });

    const result = expandSummaryNode(TEST_GROUP, summaryId!);
    expect(result).not.toBeNull();
    expect(result!.summary.content).toBe('Summary of turn one.');
    expect(result!.turns).toHaveLength(1);
    expect(result!.turns[0].content).toBe('turn one');
  });
});

describe('describeLcm', () => {
  it('returns no-history message for empty DB', () => {
    const desc = describeLcm(TEST_GROUP);
    expect(desc).toContain('Total turns: 0');
  });

  it('returns stats when turns exist', () => {
    storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: 'hi', agentContent: 'hello' });
    const desc = describeLcm(TEST_GROUP);
    expect(desc).toContain('Total turns: 2');
  });

  it('returns summary node detail when id provided', () => {
    const id = storeSummaryNode({
      groupFolder: TEST_GROUP,
      depth: 1,
      content: 'Phase summary content.',
      sourceTurnIds: ['t1', 't2'],
    });
    const desc = describeLcm(TEST_GROUP, id!);
    expect(desc).toContain('Depth: 1');
    expect(desc).toContain('Source turns: 2');
  });

  it('returns not-found for unknown id', () => {
    const desc = describeLcm(TEST_GROUP, 'unknown-id');
    expect(desc).toContain('No summary node found');
  });
});

describe('context injection integration', () => {
  it('assembleLcmContext returns empty when LCM disabled', () => {
    vi.stubEnv('LCM_ENABLED', 'false');
    // New group with no injected DB — should return empty
    expect(assembleLcmContext('no-tenant/no-group')).toBe('');
    vi.stubEnv('LCM_ENABLED', 'true');
  });

  it('assembled context wraps turns in lcm_context tags', () => {
    storeTurnPair({
      groupFolder: TEST_GROUP,
      sessionId: 'session-abc',
      userContent: 'tell me about the project',
      agentContent: 'The project is called Aurora and involves LCM.',
    });
    const ctx = assembleLcmContext(TEST_GROUP);
    expect(ctx.startsWith('<lcm_context>')).toBe(true);
    expect(ctx.endsWith('</lcm_context>')).toBe(true);
    expect(ctx).toContain('Aurora');
    expect(ctx).toContain('LCM');
  });

  it('assembled context with both summaries and fresh turns is ordered correctly', () => {
    // Old summary
    storeSummaryNode({
      groupFolder: TEST_GROUP,
      depth: 0,
      content: 'Early session: discussed requirements.',
      sourceTurnIds: [],
    });
    // Recent turn
    storeTurnPair({
      groupFolder: TEST_GROUP,
      sessionId: 's1',
      userContent: 'latest question',
      agentContent: 'latest answer',
    });
    const ctx = assembleLcmContext(TEST_GROUP);
    const summaryPos = ctx.indexOf('Early session');
    const turnPos = ctx.indexOf('latest question');
    expect(summaryPos).toBeGreaterThan(-1);
    expect(turnPos).toBeGreaterThan(-1);
    // Summary should appear before the recent turn
    expect(summaryPos).toBeLessThan(turnPos);
  });
});

describe('compaction', () => {
  it('shouldCompact returns false for empty DB', () => {
    expect(shouldCompact(TEST_GROUP)).toBe(false);
  });

  it('shouldCompact returns false when only fresh tail turns exist', () => {
    // Add 10 turns — well under the fresh tail of 32
    for (let i = 0; i < 10; i++) {
      storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: `q${i}`, agentContent: `a${i}` });
    }
    expect(shouldCompact(TEST_GROUP)).toBe(false);
  });

  it('getTurnsForCompaction returns empty when only fresh tail', () => {
    for (let i = 0; i < 5; i++) {
      storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: `q${i}`, agentContent: `a${i}` });
    }
    expect(getTurnsForCompaction(TEST_GROUP)).toHaveLength(0);
  });

  it('getTurnsForCompaction returns turns outside fresh tail', () => {
    // Add 40 turn pairs (80 rows) — 8 pairs outside the fresh tail of 32 rows
    for (let i = 0; i < 40; i++) {
      storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: `q${i}`, agentContent: `a${i}` });
    }
    const forCompaction = getTurnsForCompaction(TEST_GROUP);
    expect(forCompaction.length).toBeGreaterThan(0);
    expect(forCompaction.length).toBeLessThan(80);
  });

  it('getTurnsForCompaction excludes already-summarised turns', () => {
    // Add 40 pairs
    for (let i = 0; i < 40; i++) {
      storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: `q${i}`, agentContent: `a${i}` });
    }
    const initial = getTurnsForCompaction(TEST_GROUP);
    const turnIds = initial.map((t) => t.id);

    // Store a summary covering those turns
    storeSummaryNode({ groupFolder: TEST_GROUP, depth: 0, content: 'summary', sourceTurnIds: turnIds });

    // Now none should need compaction
    expect(getTurnsForCompaction(TEST_GROUP)).toHaveLength(0);
  });

  it('deterministicTruncate truncates text over 2048 chars', () => {
    const long = 'x'.repeat(3000);
    const result = deterministicTruncate(long);
    expect(result.length).toBeLessThan(3000);
    expect(result).toContain('[LCM: deterministic truncation applied]');
  });

  it('deterministicTruncate leaves short text unchanged', () => {
    const short = 'hello world';
    expect(deterministicTruncate(short)).toBe(short);
  });

  it('buildLeafSummaryPrompt contains conversation segment', () => {
    const prompt = buildLeafSummaryPrompt('User: hi\nAssistant: hello');
    expect(prompt).toContain('conversation_segment');
    expect(prompt).toContain('User: hi');
  });

  it('buildLeafSummaryPrompt aggressive mode mentions aggressive policy', () => {
    const prompt = buildLeafSummaryPrompt('some text', true);
    expect(prompt).toContain('Aggressive summary policy');
  });

  it('runCompaction with mock summarizer stores a summary node', async () => {
    // Add 40 pairs to trigger compaction candidates
    for (let i = 0; i < 40; i++) {
      storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: `q${i}`, agentContent: `a${i}` });
    }
    const mockSummarize = async (_prompt: string) => 'Mock summary of earlier conversation. Files: none. Expand for details about: details.';
    const result = await runCompaction(TEST_GROUP, mockSummarize);
    expect(result.compacted).toBe(true);
    expect(typeof result.summaryId).toBe('string');

    // Verify summary node was stored
    const desc = describeLcm(TEST_GROUP, result.summaryId!);
    expect(desc).toContain('Depth: 0');
  });

  it('runCompaction falls back to deterministic truncate when summarizer always fails', async () => {
    for (let i = 0; i < 40; i++) {
      storeTurnPair({ groupFolder: TEST_GROUP, sessionId: 's1', userContent: `q${i}`, agentContent: `a${i}` });
    }
    const failingSummarize = async (_prompt: string): Promise<string> => {
      throw new Error('API unavailable');
    };
    const result = await runCompaction(TEST_GROUP, failingSummarize);
    expect(result.compacted).toBe(true);
    expect(result.summaryId).not.toBeNull();
  });

  it('runCompaction returns not-compacted when no turns need compaction', async () => {
    const result = await runCompaction(TEST_GROUP, async () => 'summary');
    expect(result.compacted).toBe(false);
  });
});
