import { describe, expect, it } from 'vitest';
import {
  WorkspaceMemory,
  applyMemoryUpdate,
  renderMemorySnapshot,
  type MemoryUpdatePayload,
} from './workspace-memory.js';

describe('WorkspaceMemory.add*', () => {
  it('mints sequential ids per type and stamps source movement', () => {
    const memory = new WorkspaceMemory();
    const f1 = memory.addFact({ claim: 'a', sourceMovement: 'investigate', now: '2026-05-01T00:00:00.000Z' });
    const f2 = memory.addFact({ claim: 'b', sourceMovement: 'investigate' });
    const d1 = memory.addDecision({ text: 'd', sourceMovement: 'plan' });
    const q1 = memory.addOpenQuestion({ question: 'q', sourceMovement: 'plan' });

    expect(f1.id).toBe('f-1');
    expect(f2.id).toBe('f-2');
    expect(d1.id).toBe('d-3');
    expect(q1.id).toBe('q-4');
    expect(f1.sourceMovement).toBe('investigate');
    expect(f1.confidence).toBe('medium');
    expect(f1.observedAt).toBe('2026-05-01T00:00:00.000Z');
  });

  it('addDoNotRepeat dedupes by exact match', () => {
    const memory = new WorkspaceMemory();
    memory.addDoNotRepeat('skip foo');
    memory.addDoNotRepeat('skip foo');
    memory.addDoNotRepeat('skip bar');
    expect(memory.size().doNotRepeat).toBe(2);
  });
});

describe('WorkspaceMemory.invalidateByPath', () => {
  it('invalidates only facts/decisions whose evidence includes the path', () => {
    const memory = new WorkspaceMemory();
    memory.addFact({ claim: 'foo claim', evidencePaths: ['foo.ts'], sourceMovement: 'investigate' });
    memory.addFact({ claim: 'bar claim', evidencePaths: ['bar.ts'], sourceMovement: 'investigate' });
    memory.addFact({ claim: 'no-evidence claim', evidencePaths: [], sourceMovement: 'investigate' });
    memory.addDecision({ text: 'change foo', evidencePaths: ['foo.ts'], sourceMovement: 'plan' });

    const evicted = memory.invalidateByPath('foo.ts', 'Edit', '2026-05-01T01:00:00.000Z');

    expect(evicted).toBe(2);
    const snap = memory.snapshot();
    expect(snap.facts.map((f) => f.claim)).toEqual(['bar claim', 'no-evidence claim']);
    expect(snap.decisions).toHaveLength(0);
  });

  it('does not double-invalidate', () => {
    const memory = new WorkspaceMemory();
    memory.addFact({ claim: 'x', evidencePaths: ['foo.ts'], sourceMovement: 'investigate' });
    expect(memory.invalidateByPath('foo.ts', 'Edit')).toBe(1);
    expect(memory.invalidateByPath('foo.ts', 'Edit')).toBe(0);
  });
});

describe('WorkspaceMemory.invalidateAllFileEvidence', () => {
  it('invalidates only entries with evidence paths', () => {
    const memory = new WorkspaceMemory();
    memory.addFact({ claim: 'has evidence', evidencePaths: ['foo.ts'], sourceMovement: 'investigate' });
    memory.addFact({ claim: 'no evidence', evidencePaths: [], sourceMovement: 'investigate' });

    expect(memory.invalidateAllFileEvidence('Bash')).toBe(1);
    const snap = memory.snapshot();
    expect(snap.facts).toHaveLength(1);
    expect(snap.facts[0]!.claim).toBe('no evidence');
  });
});

describe('applyMemoryUpdate', () => {
  it('applies a well-formed payload and returns counts', () => {
    const memory = new WorkspaceMemory();
    const payload: MemoryUpdatePayload = {
      facts: [
        { claim: 'A', evidence_paths: ['foo.ts'], confidence: 'high' },
        { claim: 'B' },
      ],
      decisions: [{ text: 'pick X', evidence_paths: ['foo.ts'] }],
      open_questions: [{ question: 'why?' }],
      do_not_repeat: ['stop reading foo.ts'],
    };
    const result = applyMemoryUpdate(memory, payload, 'investigate', '2026-05-01T00:00:00.000Z');

    // Phase 6c: result shape gained `factsMerged` / `decisionsMerged` /
    // `openQuestionsMerged` for exact-claim dedup tracking.
    expect(result).toEqual({
      factsAdded: 2,
      factsMerged: 0,
      decisionsAdded: 1,
      decisionsMerged: 0,
      openQuestionsAdded: 1,
      openQuestionsMerged: 0,
      doNotRepeatAdded: 1,
      rejected: 0,
    });
    const snap = memory.snapshot();
    expect(snap.facts).toHaveLength(2);
    expect(snap.facts[0]!.confidence).toBe('high');
    expect(snap.facts[1]!.confidence).toBe('medium');
    expect(snap.facts[0]!.evidencePaths).toEqual(['foo.ts']);
    expect(snap.decisions[0]!.text).toBe('pick X');
    expect(snap.openQuestions[0]!.question).toBe('why?');
    expect(snap.doNotRepeat).toEqual(['stop reading foo.ts']);
  });

  it('rejects malformed entries without throwing', () => {
    const memory = new WorkspaceMemory();
    const payload: MemoryUpdatePayload = {
      facts: [
        { claim: '' },             // empty
        { evidence_paths: ['x'] }, // missing claim
        { claim: 'good' },
      ],
      decisions: [{ text: '' }, { text: 'good decision' }],
      open_questions: [{ question: '' }, { question: 'good?' }],
    };
    const result = applyMemoryUpdate(memory, payload, 'investigate');
    expect(result.factsAdded).toBe(1);
    expect(result.decisionsAdded).toBe(1);
    expect(result.openQuestionsAdded).toBe(1);
    expect(result.rejected).toBe(4);
  });

  it('falls back to confidence=medium when value is invalid', () => {
    const memory = new WorkspaceMemory();
    applyMemoryUpdate(memory, {
      facts: [{ claim: 'x', confidence: 'super-high' as unknown as string }],
    }, 'investigate');
    expect(memory.snapshot().facts[0]!.confidence).toBe('medium');
  });

  it('treats missing payload as no-op', () => {
    const memory = new WorkspaceMemory();
    const r = applyMemoryUpdate(memory, undefined, 'investigate');
    expect(r.factsAdded).toBe(0);
    expect(memory.size().facts).toBe(0);
  });
});

describe('renderMemorySnapshot', () => {
  it('returns empty string when snapshot is empty', () => {
    const memory = new WorkspaceMemory();
    expect(renderMemorySnapshot(memory.snapshot())).toBe('');
  });

  it('renders facts with source movement, confidence, and evidence refs', () => {
    const memory = new WorkspaceMemory();
    memory.addFact({ claim: 'foo uses bar', evidencePaths: ['foo.ts', 'bar.ts'], confidence: 'high', sourceMovement: 'investigate' });
    const out = renderMemorySnapshot(memory.snapshot());
    expect(out).toContain('## これまでに蓄積した観測');
    expect(out).toContain('### 確立した事実');
    expect(out).toContain('[investigate] (high) foo uses bar');
    // Phase 5: evidence is now split into paths/urls subgroups.
    expect(out).toContain('[evidence: paths: foo.ts, bar.ts]');
    expect(out).toContain('memory は再調査禁止の根拠ではなく');
  });

  it('caps facts at 20 entries (oldest dropped) and reports the truncation', () => {
    const memory = new WorkspaceMemory();
    for (let i = 1; i <= 25; i++) {
      memory.addFact({ claim: `fact ${i}`, sourceMovement: 'investigate' });
    }
    const out = renderMemorySnapshot(memory.snapshot());
    expect(out).toContain('20/25件、古い 5 件は省略');
    expect(out).toContain('fact 6');  // first kept
    expect(out).toContain('fact 25'); // last kept
    expect(out).not.toContain('fact 5'); // dropped
  });

  it('omits sections with no entries', () => {
    const memory = new WorkspaceMemory();
    memory.addFact({ claim: 'only fact', sourceMovement: 'investigate' });
    const out = renderMemorySnapshot(memory.snapshot());
    expect(out).toContain('### 確立した事実');
    expect(out).not.toContain('### 決定');
    expect(out).not.toContain('### 未解決の問い');
    expect(out).not.toContain('### 繰り返し禁止');
  });

  it('excludes invalidated facts', () => {
    const memory = new WorkspaceMemory();
    memory.addFact({ claim: 'foo claim alpha', evidencePaths: ['foo.ts'], sourceMovement: 'investigate' });
    memory.addFact({ claim: 'bar claim beta', evidencePaths: ['bar.ts'], sourceMovement: 'investigate' });
    memory.invalidateByPath('foo.ts', 'Edit');
    const out = renderMemorySnapshot(memory.snapshot());
    expect(out).toContain('bar claim beta');
    expect(out).not.toContain('foo claim alpha');
    expect(out).toContain('### 確立した事実 (1件)');
  });
});
