import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { MemoryStore } from './store.js';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';

describe('MemoryStore', () => {
  let dir: string;
  let store: MemoryStore;

  beforeEach(() => {
    dir = mkdtempSync(join(tmpdir(), 'flynn-memory-test-'));
    store = new MemoryStore({ dir, maxContextTokens: 4096 });
  });

  afterEach(() => {
    rmSync(dir, { recursive: true, force: true });
  });

  describe('read', () => {
    it('returns empty string for non-existent namespace', () => {
      const result = store.read('nonexistent');
      expect(result).toBe('');
    });

    it('returns file contents for existing namespace', () => {
      // Pre-populate a memory file directly on disk
      const filePath = join(dir, 'notes.md');
      writeFileSync(filePath, 'Remember to check logs daily');

      const result = store.read('notes');
      expect(result).toBe('Remember to check logs daily');
    });

    it('handles nested namespaces (sessions/abc123)', () => {
      // Pre-populate a nested memory file
      const nestedDir = join(dir, 'sessions');
      mkdirSync(nestedDir, { recursive: true });
      writeFileSync(join(nestedDir, 'abc123.md'), 'Session context data');

      const result = store.read('sessions/abc123');
      expect(result).toBe('Session context data');
    });
  });

  describe('write', () => {
    it('creates file and directories on first write', () => {
      store.write('projects/alpha', 'Initial content', 'replace');

      const filePath = join(dir, 'projects', 'alpha.md');
      const content = readFileSync(filePath, 'utf-8');
      expect(content).toContain('Initial content');
    });

    it('appends content with newline separator in append mode', () => {
      store.write('journal', 'First entry', 'replace');
      store.write('journal', 'Second entry', 'append');

      const result = store.read('journal');
      expect(result).toContain('First entry');
      expect(result).toContain('Second entry');

      // Verify entries are separated by newline
      const lines = result.split('\n').filter((l: string) => l.trim().length > 0);
      expect(lines.length).toBeGreaterThanOrEqual(2);
    });

    it('replaces content in replace mode', () => {
      store.write('config', 'Old content', 'replace');
      store.write('config', 'New content', 'replace');

      const result = store.read('config');
      expect(result).toContain('New content');
      expect(result).not.toContain('Old content');
    });

    it('handles nested namespace paths', () => {
      store.write('deep/nested/path', 'Deep content', 'replace');

      const result = store.read('deep/nested/path');
      expect(result).toContain('Deep content');
    });
  });

  describe('category APIs', () => {
    it('reads and writes category namespaces', () => {
      store.writeCategory('user', 'facts', 'User lives in Berlin', 'replace');
      expect(store.readCategory('user', 'facts')).toBe('User lives in Berlin');
    });

    it('supports append and replace modes in category writes', () => {
      store.writeCategory('user', 'preferences', 'Prefers short answers', 'replace');
      store.writeCategory('user', 'preferences', 'Likes numbered lists', 'append');
      expect(store.readCategory('user', 'preferences')).toContain('Prefers short answers');
      expect(store.readCategory('user', 'preferences')).toContain('Likes numbered lists');

      store.writeCategory('user', 'preferences', 'Only this remains', 'replace');
      const content = store.readCategory('user', 'preferences');
      expect(content).toContain('Only this remains');
      expect(content).not.toContain('Prefers short answers');
    });

    it('lists only categories that exist under a base namespace', () => {
      store.writeCategory('user', 'facts', 'Fact', 'replace');
      store.writeCategory('user', 'projects', 'Project', 'replace');
      store.writeCategory('global', 'decisions', 'Decision', 'replace');

      expect(store.listCategories('user')).toEqual(['facts', 'projects']);
      expect(store.listCategories('global')).toEqual(['decisions']);
      expect(store.listCategories('sessions/abc')).toEqual([]);
    });

    it('reads all existing categories under a base namespace', () => {
      store.writeCategory('user', 'facts', 'Fact content', 'replace');
      store.writeCategory('user', 'decisions', 'Decision content', 'replace');

      expect(store.readAllCategories('user')).toEqual({
        facts: 'Fact content',
        decisions: 'Decision content',
      });
    });
  });

  describe('search', () => {
    beforeEach(() => {
      store.write('notes', 'The quick brown fox jumps over the lazy dog\nAnother line of text\nFox sightings are common here', 'replace');
      store.write('journal', 'Today I saw a fox in the garden\nIt was a beautiful day\nThe weather was sunny', 'replace');
      store.write('config', 'No relevant animals mentioned\nJust some settings here', 'replace');
    });

    it('finds matching lines case-insensitively', () => {
      const results = store.search('fox');

      // Should match lines containing "fox" regardless of case
      expect(results.length).toBeGreaterThanOrEqual(1);

      const contents = results.map((r) => r.content.toLowerCase());
      for (const content of contents) {
        expect(content).toContain('fox');
      }
    });

    it('returns context lines around matches', () => {
      const results = store.search('fox');

      // Each result should have a context field with surrounding text
      for (const result of results) {
        expect(result.context).toBeDefined();
        expect(result.context.length).toBeGreaterThan(0);
      }
    });

    it('searches across multiple namespaces', () => {
      const results = store.search('fox');

      // Should find matches in both 'notes' and 'journal' namespaces
      const namespaces = new Set(results.map((r) => r.namespace));
      expect(namespaces.size).toBeGreaterThanOrEqual(2);
    });

    it('returns empty array when no matches', () => {
      const results = store.search('xyznonexistent');
      expect(results).toEqual([]);
    });

    it('supports filtering by category', () => {
      store.writeCategory('user', 'facts', 'fox factual statement', 'replace');
      store.writeCategory('user', 'preferences', 'prefers fox metaphors', 'replace');

      const factOnly = store.search('fox', { categories: ['facts'] });
      expect(factOnly.length).toBeGreaterThan(0);
      expect(factOnly.every(result => result.namespace.endsWith('/facts'))).toBe(true);
    });

    it('supports filtering by base namespace prefix', () => {
      store.writeCategory('user', 'facts', 'fox in user facts', 'replace');
      store.writeCategory('global', 'facts', 'fox in global facts', 'replace');

      const userOnly = store.search('fox', { baseNamespacePrefix: 'user/' });
      expect(userOnly.length).toBeGreaterThan(0);
      expect(userOnly.every(result => result.namespace.startsWith('user/'))).toBe(true);
    });
  });

  describe('listNamespaces', () => {
    it('returns empty array when no files exist', () => {
      const namespaces = store.listNamespaces();
      expect(namespaces).toEqual([]);
    });

    it('lists all namespaces from existing files', () => {
      store.write('notes', 'Some notes', 'replace');
      store.write('journal', 'A journal entry', 'replace');
      store.write('config', 'Configuration data', 'replace');

      const namespaces = store.listNamespaces();

      expect(namespaces).toContain('notes');
      expect(namespaces).toContain('journal');
      expect(namespaces).toContain('config');
      expect(namespaces).toHaveLength(3);
    });

    it('includes nested namespaces', () => {
      store.write('sessions/abc123', 'Session data', 'replace');
      store.write('sessions/def456', 'Another session', 'replace');
      store.write('global', 'Global notes', 'replace');

      const namespaces = store.listNamespaces();

      expect(namespaces).toContain('sessions/abc123');
      expect(namespaces).toContain('sessions/def456');
      expect(namespaces).toContain('global');
    });
  });

  describe('getContextForPrompt', () => {
    it('returns empty string when no memory files exist', () => {
      const context = store.getContextForPrompt();
      expect(context).toBe('');
    });

    it('includes user and global memory under headings', () => {
      store.write('user', 'User prefers concise answers', 'replace');
      store.write('global', 'System-wide knowledge base', 'replace');
      store.writeCategory('user', 'facts', 'User timezone is UTC', 'replace');
      store.writeCategory('global', 'decisions', 'Adopt pnpm workspace', 'replace');

      const context = store.getContextForPrompt();

      // Should contain both memory sections with some form of heading
      expect(context).toContain('User Memory');
      expect(context).toContain('User prefers concise answers');
      expect(context).toContain('Global Memory');
      expect(context).toContain('System-wide knowledge base');
      expect(context).toContain('User Facts');
      expect(context).toContain('User timezone is UTC');
      expect(context).toContain('Global Decisions');
      expect(context).toContain('Adopt pnpm workspace');
    });

    it('truncates content to stay within maxContextTokens', () => {
      // Create a store with a very small token limit (50 tokens ≈ 200 chars)
      const smallStore = new MemoryStore({ dir, maxContextTokens: 50 });

      // Write a large amount of content to user and global (which getContextForPrompt reads)
      const longContent = 'A'.repeat(5000);
      smallStore.write('user', longContent, 'replace');
      smallStore.write('global', longContent, 'replace');

      const context = smallStore.getContextForPrompt();

      // 50 tokens * 4 chars/token = 200 chars max
      expect(context.length).toBeLessThanOrEqual(200);
      expect(context.length).toBeLessThan(longContent.length);
    });
  });
});
