import { describe, it, expect, beforeEach } from 'vitest';
import type { StorageAdapter } from '@mosaicstack/storage';
import { KeywordAdapter } from './keyword.js';

/* ------------------------------------------------------------------ */
/*  In-memory mock StorageAdapter                                     */
/* ------------------------------------------------------------------ */

function createMockStorage(): StorageAdapter {
  const collections = new Map<string, Map<string, Record<string, unknown>>>();
  let idCounter = 0;

  function getCollection(name: string): Map<string, Record<string, unknown>> {
    if (!collections.has(name)) collections.set(name, new Map());
    return collections.get(name)!;
  }

  const adapter: StorageAdapter = {
    name: 'mock',

    async create<T extends Record<string, unknown>>(
      collection: string,
      data: T,
    ): Promise<T & { id: string }> {
      const id = String(++idCounter);
      const record = { ...data, id };
      getCollection(collection).set(id, record);
      return record as T & { id: string };
    },

    async read<T extends Record<string, unknown>>(
      collection: string,
      id: string,
    ): Promise<T | null> {
      const record = getCollection(collection).get(id);
      return (record as T) ?? null;
    },

    async update(collection: string, id: string, data: Record<string, unknown>): Promise<boolean> {
      const col = getCollection(collection);
      const existing = col.get(id);
      if (!existing) return false;
      col.set(id, { ...existing, ...data });
      return true;
    },

    async delete(collection: string, id: string): Promise<boolean> {
      return getCollection(collection).delete(id);
    },

    async find<T extends Record<string, unknown>>(
      collection: string,
      filter?: Record<string, unknown>,
    ): Promise<T[]> {
      const col = getCollection(collection);
      const results: T[] = [];
      for (const record of col.values()) {
        if (filter && !matchesFilter(record, filter)) continue;
        results.push(record as T);
      }
      return results;
    },

    async findOne<T extends Record<string, unknown>>(
      collection: string,
      filter: Record<string, unknown>,
    ): Promise<T | null> {
      const col = getCollection(collection);
      for (const record of col.values()) {
        if (matchesFilter(record, filter)) return record as T;
      }
      return null;
    },

    async count(collection: string, filter?: Record<string, unknown>): Promise<number> {
      const rows = await adapter.find(collection, filter);
      return rows.length;
    },

    async transaction<T>(fn: (tx: StorageAdapter) => Promise<T>): Promise<T> {
      return fn(adapter);
    },

    async migrate(): Promise<void> {},
    async close(): Promise<void> {},
  };

  return adapter;
}

function matchesFilter(record: Record<string, unknown>, filter: Record<string, unknown>): boolean {
  for (const [key, value] of Object.entries(filter)) {
    if (record[key] !== value) return false;
  }
  return true;
}

/* ------------------------------------------------------------------ */
/*  Tests                                                             */
/* ------------------------------------------------------------------ */

describe('KeywordAdapter', () => {
  let adapter: KeywordAdapter;

  beforeEach(() => {
    adapter = new KeywordAdapter({ type: 'keyword', storage: createMockStorage() });
  });

  /* ---- Preferences ---- */

  describe('preferences', () => {
    it('should set and get a preference', async () => {
      await adapter.setPreference('u1', 'theme', 'dark');
      const value = await adapter.getPreference('u1', 'theme');
      expect(value).toBe('dark');
    });

    it('should return null for missing preference', async () => {
      const value = await adapter.getPreference('u1', 'nonexistent');
      expect(value).toBeNull();
    });

    it('should upsert an existing preference', async () => {
      await adapter.setPreference('u1', 'theme', 'dark');
      await adapter.setPreference('u1', 'theme', 'light');
      const value = await adapter.getPreference('u1', 'theme');
      expect(value).toBe('light');
    });

    it('should delete a preference', async () => {
      await adapter.setPreference('u1', 'theme', 'dark');
      const deleted = await adapter.deletePreference('u1', 'theme');
      expect(deleted).toBe(true);
      const value = await adapter.getPreference('u1', 'theme');
      expect(value).toBeNull();
    });

    it('should return false when deleting nonexistent preference', async () => {
      const deleted = await adapter.deletePreference('u1', 'nope');
      expect(deleted).toBe(false);
    });

    it('should list preferences by userId', async () => {
      await adapter.setPreference('u1', 'theme', 'dark', 'appearance');
      await adapter.setPreference('u1', 'lang', 'en', 'locale');
      await adapter.setPreference('u2', 'theme', 'light', 'appearance');

      const prefs = await adapter.listPreferences('u1');
      expect(prefs).toHaveLength(2);
      expect(prefs.map((p) => p.key).sort()).toEqual(['lang', 'theme']);
    });

    it('should filter preferences by category', async () => {
      await adapter.setPreference('u1', 'theme', 'dark', 'appearance');
      await adapter.setPreference('u1', 'lang', 'en', 'locale');

      const prefs = await adapter.listPreferences('u1', 'appearance');
      expect(prefs).toHaveLength(1);
      expect(prefs[0]!.key).toBe('theme');
    });
  });

  /* ---- Insights ---- */

  describe('insights', () => {
    it('should store and retrieve an insight', async () => {
      const insight = await adapter.storeInsight({
        userId: 'u1',
        content: 'TypeScript is great for type safety',
        source: 'chat',
        category: 'technical',
        relevanceScore: 0.9,
      });

      expect(insight.id).toBeDefined();
      expect(insight.content).toBe('TypeScript is great for type safety');

      const fetched = await adapter.getInsight(insight.id);
      expect(fetched).not.toBeNull();
      expect(fetched!.content).toBe('TypeScript is great for type safety');
    });

    it('should return null for missing insight', async () => {
      const result = await adapter.getInsight('nonexistent');
      expect(result).toBeNull();
    });

    it('should delete an insight', async () => {
      const insight = await adapter.storeInsight({
        userId: 'u1',
        content: 'test',
        source: 'chat',
        category: 'general',
        relevanceScore: 0.5,
      });

      const deleted = await adapter.deleteInsight(insight.id);
      expect(deleted).toBe(true);

      const fetched = await adapter.getInsight(insight.id);
      expect(fetched).toBeNull();
    });
  });

  /* ---- Keyword Search ---- */

  describe('searchInsights', () => {
    beforeEach(async () => {
      await adapter.storeInsight({
        userId: 'u1',
        content: 'TypeScript provides excellent type safety for JavaScript projects',
        source: 'chat',
        category: 'technical',
        relevanceScore: 0.9,
      });
      await adapter.storeInsight({
        userId: 'u1',
        content: 'React hooks simplify state management in components',
        source: 'chat',
        category: 'technical',
        relevanceScore: 0.8,
      });
      await adapter.storeInsight({
        userId: 'u1',
        content: 'TypeScript and React work great together for type safe components',
        source: 'chat',
        category: 'technical',
        relevanceScore: 0.85,
      });
      await adapter.storeInsight({
        userId: 'u2',
        content: 'TypeScript is popular',
        source: 'chat',
        category: 'general',
        relevanceScore: 0.5,
      });
    });

    it('should find insights by exact keyword', async () => {
      const results = await adapter.searchInsights('u1', 'hooks');
      expect(results).toHaveLength(1);
      expect(results[0]!.content).toContain('hooks');
    });

    it('should be case-insensitive', async () => {
      const results = await adapter.searchInsights('u1', 'TYPESCRIPT');
      expect(results.length).toBeGreaterThanOrEqual(1);
      for (const r of results) {
        expect(r.content.toLowerCase()).toContain('typescript');
      }
    });

    it('should rank multi-word matches higher', async () => {
      const results = await adapter.searchInsights('u1', 'TypeScript React');
      // The insight mentioning both "TypeScript" and "React" should rank first (score=2)
      expect(results[0]!.score).toBe(2);
      expect(results[0]!.content).toContain('TypeScript');
      expect(results[0]!.content).toContain('React');
    });

    it('should return empty for no matches', async () => {
      const results = await adapter.searchInsights('u1', 'python django');
      expect(results).toHaveLength(0);
    });

    it('should filter by userId', async () => {
      const results = await adapter.searchInsights('u2', 'TypeScript');
      expect(results).toHaveLength(1);
      expect(results[0]!.content).toBe('TypeScript is popular');
    });

    it('should respect limit option', async () => {
      const results = await adapter.searchInsights('u1', 'TypeScript', { limit: 1 });
      expect(results).toHaveLength(1);
    });

    it('should return all scoped insights for the explicit wildcard query', async () => {
      await adapter.storeInsight({
        userId: 'u1',
        content: 'A literal * marker is still ordinary content',
        source: 'chat',
        category: 'technical',
        relevanceScore: 0.7,
      });

      const results = await adapter.searchInsights('u1', '*');
      expect(results).toHaveLength(4);
      expect(results.every((result) => result.score === 1)).toBe(true);
    });

    it('should return empty for empty query', async () => {
      const results = await adapter.searchInsights('u1', '   ');
      expect(results).toHaveLength(0);
    });
  });

  /* ---- Lifecycle ---- */

  describe('lifecycle', () => {
    it('should have name "keyword"', () => {
      expect(adapter.name).toBe('keyword');
    });

    it('should have null embedder', () => {
      expect(adapter.embedder).toBeNull();
    });

    it('should close without error', async () => {
      await expect(adapter.close()).resolves.toBeUndefined();
    });
  });
});
