/**
 * Tests for Agentic Memory Backend (A-MEM).
 * (Source: Issue #122, arXiv:2502.12110)
 */

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
  AgenticMemoryBackend,
  createAgenticMemory,
  DEFAULT_EXTRACTION_CONFIG,
  DEFAULT_LINKING_CONFIG,
} from './agentic-memory.js';
import {
  extractKeywords,
  extractSemanticTags,
  extractEntities,
  generateContextDescription,
  mergeExtractionConfig,
} from './agentic-memory-extraction.js';
import {
  calculateKeywordSimilarity,
  calculateEntitySimilarity,
  calculateOverallSimilarity,
  generateLinkSuggestions,
  detectEvolution,
  mergeLinkingConfig,
} from './agentic-memory-linking.js';
// Shared utilities per ADR-0013
import { tokenize, tokenizeFiltered } from '../utils/text-utils.js';
import type { ISQLiteDatabase, ISQLiteStatement, MemoryRow } from './memory-backend-types.js';
import { MemoryImportance } from './memory-backend-types.js';
import type { MemoryAttributes, EntityReference } from './agentic-memory-types.js';

// =============================================================================
// Mock SQLite Database
// =============================================================================

interface MockStore {
  memories: Map<string, MemoryRow>;
  edges: Map<string, { from: string; to: string; type: string; weight: number }>;
}

function handleMemoryInsert(store: MockStore, params: unknown[]): { changes: number } {
  const [key, value, metadata, created_at, accessed_at, expires_at] = params as [
    string,
    string,
    string,
    number,
    number,
    number | null,
  ];
  store.memories.set(key, { key, value, metadata, created_at, accessed_at, expires_at });
  return { changes: 1 };
}

function handleMemoryUpdate(store: MockStore, params: unknown[]): { changes: number } {
  if (params.length === 2) {
    const [accessed_at, key] = params as [number, string];
    const row = store.memories.get(key);
    if (row !== undefined) {
      store.memories.set(key, { ...row, accessed_at });
      return { changes: 1 };
    }
  } else if (params.length === 2) {
    const [metadata, key] = params as [string, string];
    const row = store.memories.get(key);
    if (row !== undefined) {
      store.memories.set(key, { ...row, metadata });
      return { changes: 1 };
    }
  }
  return { changes: 0 };
}

function handleEdgeInsert(store: MockStore, params: unknown[]): { changes: number } {
  const [from, to, type, weight] = params as [string, string, string, number];
  const edgeKey = `${from}:${to}:${type}`;
  store.edges.set(edgeKey, { from, to, type, weight });
  return { changes: 1 };
}

function createMockRun(
  store: MockStore,
  sql: string
): (...params: unknown[]) => { changes: number } {
  return (...params: unknown[]): { changes: number } => {
    if (sql.includes('INSERT') && sql.includes('memories'))
      return handleMemoryInsert(store, params);
    if (sql.includes('UPDATE') && sql.includes('memories'))
      return handleMemoryUpdate(store, params);
    if (sql.includes('INSERT') && sql.includes('graph_edges'))
      return handleEdgeInsert(store, params);
    if (sql.includes('DELETE')) {
      const [key] = params as [string];
      const had = store.memories.has(key);
      store.memories.delete(key);
      return { changes: had ? 1 : 0 };
    }
    return { changes: 0 };
  };
}

function createMockGet(store: MockStore, sql: string): (...params: unknown[]) => unknown {
  // eslint-disable-next-line complexity -- test mock with multiple SQL patterns
  return (...params: unknown[]): unknown => {
    if (sql.includes('COUNT') && sql.includes('memories')) {
      // Param-less `SELECT COUNT(*) FROM memories` → total row count.
      if (params.length === 0) return { count: store.memories.size };
      const [key] = params as [string];
      return { count: store.memories.has(key) ? 1 : 0 };
    }
    if (sql.includes('SELECT') && sql.includes('memories') && sql.includes('key = ?')) {
      const [key] = params as [string];
      return store.memories.get(key);
    }
    if (sql.includes('SELECT') && sql.includes('memories') && sql.includes('key != ?')) {
      // This is for get(), return first match
      const [excludeKey] = params as [string];
      for (const row of store.memories.values()) {
        if (row.key !== excludeKey) return row;
      }
      return undefined;
    }
    return undefined;
  };
}

function createMockAll(store: MockStore, sql: string): (...params: unknown[]) => unknown[] {
  return (...params: unknown[]): unknown[] => {
    if (sql.includes('memories') && sql.includes('key != ?')) {
      const [excludeKey] = params as [string];
      return Array.from(store.memories.values())
        .filter((m) => m.key !== excludeKey)
        .sort((a, b) => b.accessed_at - a.accessed_at)
        .slice(0, 100);
    }
    if (sql.includes('memories') && sql.includes('ORDER BY')) {
      const [limit] = params as [number];
      return Array.from(store.memories.values())
        .sort((a, b) => b.accessed_at - a.accessed_at)
        .slice(0, limit);
    }
    if (sql.includes('memories_fts')) {
      // FTS search - return memories matching query
      const [query, limit] = params as [string, number];
      const queryLower = query.toLowerCase();
      return Array.from(store.memories.values())
        .filter((m) => m.value.toLowerCase().includes(queryLower))
        .slice(0, limit);
    }
    return [];
  };
}

function createMockDatabase(): ISQLiteDatabase & { store: MockStore } {
  const store: MockStore = { memories: new Map(), edges: new Map() };
  return {
    store,
    exec: (): void => {},
    prepare: <T = unknown>(sql: string): ISQLiteStatement<T> => ({
      run: createMockRun(store, sql),
      get: createMockGet(store, sql) as ISQLiteStatement<T>['get'],
      all: createMockAll(store, sql) as ISQLiteStatement<T>['all'],
    }),
    close: (): void => {
      store.memories.clear();
      store.edges.clear();
    },
  };
}

// =============================================================================
// Test Configuration
// =============================================================================

const testConfig = { dbPath: ':memory:', markdownDir: '/tmp/test-markdown' };

// =============================================================================
// Helper Functions
// =============================================================================

function createMockAttributes(
  keywords: string[],
  entities: EntityReference[] = []
): MemoryAttributes {
  return {
    keywords,
    semanticTags: [],
    contextDescription: 'Test context',
    entities,
    attributesUpdatedAt: new Date(),
  };
}

// =============================================================================
// Tokenization Tests
// =============================================================================

describe('tokenize', () => {
  it('should split text into lowercase words', () => {
    expect(tokenize('Hello World')).toEqual(['hello', 'world']);
  });

  it('should remove punctuation', () => {
    expect(tokenize('Hello, World!')).toEqual(['hello', 'world']);
  });

  it('should filter single-character tokens', () => {
    expect(tokenize('a b c foo bar')).toEqual(['foo', 'bar']);
  });
});

describe('tokenizeFiltered', () => {
  it('should remove stopwords', () => {
    const result = tokenizeFiltered('the quick brown fox');
    expect(result).toContain('quick');
    expect(result).toContain('brown');
    expect(result).toContain('fox');
    expect(result).not.toContain('the');
  });
});

// =============================================================================
// Keyword Extraction Tests
// =============================================================================

describe('extractKeywords', () => {
  it('should extract most frequent words', () => {
    const text = 'typescript typescript typescript code function code';
    const keywords = extractKeywords(text, 5);
    expect(keywords[0]).toBe('typescript');
    expect(keywords[1]).toBe('code');
  });

  it('should respect maxKeywords limit', () => {
    const text = 'one two three four five six seven eight nine ten';
    const keywords = extractKeywords(text, 3);
    expect(keywords.length).toBe(3);
  });

  it('should return empty array for empty text', () => {
    expect(extractKeywords('', 5)).toEqual([]);
  });
});

// =============================================================================
// Semantic Tag Tests
// =============================================================================

describe('extractSemanticTags', () => {
  it('should extract code-related tags', () => {
    const text = 'function createUser implements interface';
    const tags = extractSemanticTags(text, 5);
    expect(tags).toContain('code');
  });

  it('should extract security-related tags', () => {
    const text = 'authentication token security credential';
    const tags = extractSemanticTags(text, 5);
    expect(tags).toContain('security');
  });

  it('should extract testing-related tags', () => {
    const text = 'test describe expect mock assert';
    const tags = extractSemanticTags(text, 5);
    expect(tags).toContain('testing');
  });

  it('should respect maxTags limit', () => {
    const text = 'function test security api database config performance';
    const tags = extractSemanticTags(text, 2);
    expect(tags.length).toBeLessThanOrEqual(2);
  });
});

// =============================================================================
// Entity Extraction Tests
// =============================================================================

describe('extractEntities', () => {
  it('should extract file paths', () => {
    const text = 'Check the file at ./src/index.ts for details';
    const entities = extractEntities(text, 10);
    expect(entities.some((e) => e.type === 'file')).toBe(true);
  });

  it('should extract CamelCase identifiers', () => {
    const text = 'The AgenticMemoryBackend class handles storage';
    const entities = extractEntities(text, 10);
    expect(entities.some((e) => e.name === 'AgenticMemoryBackend')).toBe(true);
  });

  it('should filter potential PII (SSN pattern)', () => {
    const text = 'SSN is 123-45-6789';
    const entities = extractEntities(text, 10);
    expect(entities.every((e) => e.name !== '123-45-6789')).toBe(true);
  });

  it('should filter potential PII (phone pattern)', () => {
    const text = 'Call 555-123-4567';
    const entities = extractEntities(text, 10);
    expect(entities.every((e) => e.name !== '555-123-4567')).toBe(true);
  });

  it('should respect maxEntities limit', () => {
    const text = 'ClassName1 ClassName2 ClassName3 ClassName4 ClassName5';
    const entities = extractEntities(text, 2);
    expect(entities.length).toBeLessThanOrEqual(2);
  });
});

// =============================================================================
// Context Description Tests
// =============================================================================

describe('generateContextDescription', () => {
  it('should extract first sentence if short enough', () => {
    const text = 'This is the first sentence. This is the second.';
    const desc = generateContextDescription(text, 200);
    expect(desc).toBe('This is the first sentence.');
  });

  it('should truncate long text with ellipsis', () => {
    const longText = 'a '.repeat(200);
    const desc = generateContextDescription(longText, 50);
    expect(desc.length).toBeLessThanOrEqual(53); // 50 + '...'
    expect(desc.endsWith('...')).toBe(true);
  });

  it('should return full text if under limit', () => {
    const text = 'Short text';
    const desc = generateContextDescription(text, 200);
    expect(desc).toBe('Short text');
  });
});

// =============================================================================
// Similarity Calculation Tests
// =============================================================================

describe('calculateKeywordSimilarity', () => {
  it('should return 1.0 for identical keywords', () => {
    const attrs1 = createMockAttributes(['foo', 'bar', 'baz']);
    const attrs2 = createMockAttributes(['foo', 'bar', 'baz']);
    expect(calculateKeywordSimilarity(attrs1, attrs2)).toBe(1.0);
  });

  it('should return 0 for no overlap', () => {
    const attrs1 = createMockAttributes(['foo', 'bar']);
    const attrs2 = createMockAttributes(['baz', 'qux']);
    expect(calculateKeywordSimilarity(attrs1, attrs2)).toBe(0);
  });

  it('should return partial score for some overlap', () => {
    const attrs1 = createMockAttributes(['foo', 'bar', 'baz']);
    const attrs2 = createMockAttributes(['foo', 'bar', 'qux']);
    const similarity = calculateKeywordSimilarity(attrs1, attrs2);
    expect(similarity).toBeGreaterThan(0);
    expect(similarity).toBeLessThan(1);
  });

  it('should return 0 for empty keywords', () => {
    const attrs1 = createMockAttributes([]);
    const attrs2 = createMockAttributes([]);
    expect(calculateKeywordSimilarity(attrs1, attrs2)).toBe(0);
  });
});

describe('calculateEntitySimilarity', () => {
  it('should return 1.0 for identical entities', () => {
    const entity = [{ name: 'TestClass', type: 'code' as const }];
    const attrs1 = createMockAttributes([], entity);
    const attrs2 = createMockAttributes([], entity);
    expect(calculateEntitySimilarity(attrs1, attrs2)).toBe(1.0);
  });

  it('should be case-insensitive', () => {
    const attrs1 = createMockAttributes([], [{ name: 'TestClass', type: 'code' as const }]);
    const attrs2 = createMockAttributes([], [{ name: 'testclass', type: 'code' as const }]);
    expect(calculateEntitySimilarity(attrs1, attrs2)).toBe(1.0);
  });
});

describe('calculateOverallSimilarity', () => {
  it('should combine keyword and entity similarity', () => {
    const attrs1 = createMockAttributes(['foo', 'bar']);
    const attrs2 = createMockAttributes(['foo', 'bar']);
    // 60% keywords (1.0) + 40% entities (0) = 0.6
    expect(calculateOverallSimilarity(attrs1, attrs2)).toBeCloseTo(0.6, 1);
  });
});

// =============================================================================
// Link Suggestion Tests
// =============================================================================

describe('generateLinkSuggestions', () => {
  it('should suggest links for memories with keyword overlap', () => {
    const sourceAttrs = createMockAttributes(['typescript', 'memory', 'backend']);
    const candidate = {
      key: 'candidate1',
      attrs: createMockAttributes(['typescript', 'memory', 'storage']),
      createdAt: new Date(Date.now() - 1000),
    };

    const suggestions = generateLinkSuggestions('source', sourceAttrs, new Date(), [candidate], {
      ...DEFAULT_LINKING_CONFIG,
      suggestionThreshold: 0.3,
    });

    expect(suggestions.length).toBeGreaterThan(0);
    const firstSuggestion = suggestions[0];
    expect(firstSuggestion).toBeDefined();
    expect(firstSuggestion?.to).toBe('candidate1');
  });

  it('should respect suggestion threshold', () => {
    const sourceAttrs = createMockAttributes(['unique', 'keywords']);
    const candidate = {
      key: 'candidate1',
      attrs: createMockAttributes(['different', 'words']),
      createdAt: new Date(),
    };

    const suggestions = generateLinkSuggestions('source', sourceAttrs, new Date(), [candidate], {
      ...DEFAULT_LINKING_CONFIG,
      suggestionThreshold: 0.9,
    });

    expect(suggestions.length).toBe(0);
  });

  it('should skip self-references', () => {
    const attrs = createMockAttributes(['foo', 'bar']);
    const candidate = {
      key: 'source', // Same as source key
      attrs,
      createdAt: new Date(),
    };

    const suggestions = generateLinkSuggestions(
      'source',
      attrs,
      new Date(),
      [candidate],
      DEFAULT_LINKING_CONFIG
    );

    expect(suggestions.length).toBe(0);
  });
});

// =============================================================================
// Evolution Detection Tests
// =============================================================================

describe('detectEvolution', () => {
  it('should detect supersession for highly similar memories', () => {
    // Need >80% similarity for supersession. With keywords only (no entities),
    // we need very high keyword overlap: similarity = 0.6 * keyword_sim + 0.4 * entity_sim
    // For supersession (>0.8): 0.6 * keyword_sim >= 0.8 means keyword_sim >= 1.33 (impossible)
    // So we need entities too, or just check for refinement instead
    const newAttrs = createMockAttributes(['api', 'endpoint', 'rest', 'http']);
    const existingMemory = {
      key: 'existing1',
      attrs: createMockAttributes(['api', 'endpoint', 'rest', 'http']),
      createdAt: new Date(Date.now() - 10000),
    };

    const evolution = detectEvolution('new', newAttrs, new Date(), [existingMemory]);

    // With 100% keyword overlap (0.6) and 0% entity overlap (0.0), similarity = 0.6
    // This triggers refinement (similarity >= 0.5 but < 0.8), not supersession
    expect(evolution.some((e) => e.type === 'refinement')).toBe(true);
  });

  it('should detect extension for memories with new concepts', () => {
    const newAttrs = createMockAttributes([
      'api',
      'endpoint',
      'graphql',
      'mutation',
      'subscription',
    ]);
    const existingMemory = {
      key: 'existing1',
      attrs: createMockAttributes(['api', 'endpoint']),
      createdAt: new Date(Date.now() - 10000),
    };

    const evolution = detectEvolution('new', newAttrs, new Date(), [existingMemory]);

    // Should detect some evolution with moderate similarity
    expect(evolution.length).toBeGreaterThanOrEqual(0);
  });

  it('should skip low-similarity memories', () => {
    const newAttrs = createMockAttributes(['completely', 'different', 'keywords']);
    const existingMemory = {
      key: 'existing1',
      attrs: createMockAttributes(['unrelated', 'other', 'words']),
      createdAt: new Date(Date.now() - 10000),
    };

    const evolution = detectEvolution('new', newAttrs, new Date(), [existingMemory]);

    expect(evolution.length).toBe(0);
  });
});

// =============================================================================
// Configuration Tests
// =============================================================================

describe('mergeExtractionConfig', () => {
  it('should return defaults when undefined', () => {
    const config = mergeExtractionConfig(undefined);
    expect(config).toEqual(DEFAULT_EXTRACTION_CONFIG);
  });

  it('should merge partial config with defaults', () => {
    const config = mergeExtractionConfig({ maxKeywords: 20 });
    expect(config.maxKeywords).toBe(20);
    expect(config.maxSemanticTags).toBe(DEFAULT_EXTRACTION_CONFIG.maxSemanticTags);
  });
});

describe('mergeLinkingConfig', () => {
  it('should return defaults when undefined', () => {
    const config = mergeLinkingConfig(undefined);
    expect(config).toEqual(DEFAULT_LINKING_CONFIG);
  });

  it('should merge partial config with defaults', () => {
    const config = mergeLinkingConfig({ suggestionThreshold: 0.8 });
    expect(config.suggestionThreshold).toBe(0.8);
    expect(config.maxSuggestions).toBe(DEFAULT_LINKING_CONFIG.maxSuggestions);
  });
});

// =============================================================================
// AgenticMemoryBackend Integration Tests
// =============================================================================

describe('AgenticMemoryBackend', () => {
  let backend: AgenticMemoryBackend;
  let mockDb: ISQLiteDatabase & { store: MockStore };

  beforeEach(() => {
    mockDb = createMockDatabase();
    backend = new AgenticMemoryBackend(testConfig);
    backend.initializeWithDatabase(mockDb);
  });

  afterEach(() => {
    backend.close();
  });

  describe('count', () => {
    it('delegates to base and returns row total', async () => {
      mockDb.store.memories.set('a', {
        key: 'a',
        value: '"v1"',
        metadata: '{}',
        created_at: Date.now(),
        accessed_at: Date.now(),
        expires_at: null,
      });
      mockDb.store.memories.set('b', {
        key: 'b',
        value: '"v2"',
        metadata: '{}',
        created_at: Date.now(),
        accessed_at: Date.now(),
        expires_at: null,
      });
      const result = await backend.count();
      expect(result.ok).toBe(true);
      if (result.ok) expect(result.value).toBe(2);
    });

    it('returns 0 on empty store', async () => {
      const result = await backend.count();
      expect(result.ok).toBe(true);
      if (result.ok) expect(result.value).toBe(0);
    });
  });

  describe('storeWithAttributes', () => {
    it('should extract and store attributes', async () => {
      // Use 'function' and 'class' (not plurals) to match code patterns
      const result = await backend.storeWithAttributes(
        'test-key',
        'This is a test about TypeScript function and class definitions',
        { importance: MemoryImportance.HIGH }
      );

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value.entry.key).toBe('test-key');
        expect(result.value.entry.attributes.keywords.length).toBeGreaterThan(0);
        // Should detect 'testing' (from 'test') and 'code' (from 'function'/'class')
        expect(result.value.entry.attributes.semanticTags).toContain('code');
      }
    });

    it('should generate link suggestions', async () => {
      // Store first memory
      await backend.storeWithAttributes('memory-1', 'TypeScript memory backend implementation', {
        importance: MemoryImportance.MEDIUM,
      });

      // Store second memory with overlap
      const result = await backend.storeWithAttributes(
        'memory-2',
        'TypeScript storage backend code',
        { importance: MemoryImportance.MEDIUM }
      );

      expect(result.ok).toBe(true);
      if (result.ok) {
        // May or may not have suggestions depending on threshold
        expect(Array.isArray(result.value.linkSuggestions)).toBe(true);
      }
    });
  });

  describe('retrieveWithAttributes', () => {
    it('should retrieve memory with attributes', async () => {
      await backend.storeWithAttributes('test-key', 'Test content with keywords', {
        importance: MemoryImportance.MEDIUM,
      });

      const result = await backend.retrieveWithAttributes('test-key');

      expect(result.ok).toBe(true);
      if (result.ok && result.value !== null) {
        expect(result.value.key).toBe('test-key');
        expect(result.value.attributes).toBeDefined();
      }
    });

    it('should return null for non-existent key', async () => {
      const result = await backend.retrieveWithAttributes('non-existent');

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value).toBeNull();
      }
    });
  });

  describe('suggestLinks', () => {
    it('should return error for non-existent source', async () => {
      const result = await backend.suggestLinks('non-existent');

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.message).toContain('not found');
      }
    });
  });

  describe('configuration', () => {
    it('should return default extraction config', () => {
      const config = backend.getExtractionConfig();
      expect(config.maxKeywords).toBe(DEFAULT_EXTRACTION_CONFIG.maxKeywords);
    });

    it('should update extraction config', () => {
      backend.updateExtractionConfig({ maxKeywords: 20 });
      const config = backend.getExtractionConfig();
      expect(config.maxKeywords).toBe(20);
    });

    it('should return default linking config', () => {
      const config = backend.getLinkingConfig();
      expect(config.suggestionThreshold).toBe(DEFAULT_LINKING_CONFIG.suggestionThreshold);
    });

    it('should update linking config', () => {
      backend.updateLinkingConfig({ suggestionThreshold: 0.9 });
      const config = backend.getLinkingConfig();
      expect(config.suggestionThreshold).toBe(0.9);
    });
  });
});

// =============================================================================
// Factory Function Tests
// =============================================================================

describe('createAgenticMemory', () => {
  it('should create an AgenticMemoryBackend instance', () => {
    const backend = createAgenticMemory(testConfig);
    expect(backend).toBeInstanceOf(AgenticMemoryBackend);
    backend.close();
  });

  it('should throw on invalid config', () => {
    expect(() => createAgenticMemory({ dbPath: '', markdownDir: '' })).toThrow();
  });
});
