/**
 * Tests for MemoryDatastoreProvider and filter evaluation.
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { MemoryDatastoreProvider } from '../providers/memory.js';
import { matchesFilter, filterToCosmosSQL } from '../filter.js';
import type { BaseDocument, DocumentCollection } from '../types.js';

interface TestDoc extends BaseDocument {
  name: string;
  price?: number;
  tags?: string[];
  createdAt?: string;
}

describe('MemoryDatastoreProvider', () => {
  let provider: MemoryDatastoreProvider;
  let collection: DocumentCollection<TestDoc>;

  beforeEach(() => {
    provider = new MemoryDatastoreProvider();
    collection = provider.getCollection<TestDoc>('test', '/productId');
  });

  it('isHealthy returns true', async () => {
    expect(await provider.isHealthy()).toBe(true);
  });

  it('returns same collection instance for same name', () => {
    const c1 = provider.getCollection('test', '/productId');
    const c2 = provider.getCollection('test', '/productId');
    expect(c1).toBe(c2);
  });

  describe('CRUD operations', () => {
    const doc: TestDoc = { id: '1', productId: 'test', name: 'Widget', price: 10 };

    it('create + findById', async () => {
      const created = await collection.create(doc);
      expect(created).toEqual(doc);
      const found = await collection.findById('1', 'test');
      expect(found).toEqual(doc);
    });

    it('create throws on duplicate', async () => {
      await collection.create(doc);
      await expect(collection.create(doc)).rejects.toThrow('already exists');
    });

    it('findById returns null for missing', async () => {
      expect(await collection.findById('missing', 'test')).toBeNull();
    });

    it('update merges fields', async () => {
      await collection.create(doc);
      const updated = await collection.update('1', 'test', { price: 20 });
      expect(updated.price).toBe(20);
      expect(updated.name).toBe('Widget');
    });

    it('update throws for missing doc', async () => {
      await expect(collection.update('missing', 'test', { price: 20 })).rejects.toThrow(
        'not found'
      );
    });

    it('upsert creates or replaces', async () => {
      const created = await collection.upsert(doc);
      expect(created).toEqual(doc);
      const replaced = await collection.upsert({ ...doc, name: 'Gadget' });
      expect(replaced.name).toBe('Gadget');
    });

    it('delete removes doc', async () => {
      await collection.create(doc);
      await collection.delete('1', 'test');
      expect(await collection.findById('1', 'test')).toBeNull();
    });
  });

  describe('findMany', () => {
    const docs: TestDoc[] = [
      { id: '1', productId: 'p1', name: 'Alpha', price: 30, createdAt: '2026-01-01' },
      { id: '2', productId: 'p1', name: 'Beta', price: 10, createdAt: '2026-01-02' },
      { id: '3', productId: 'p1', name: 'Gamma', price: 20, createdAt: '2026-01-03' },
      { id: '4', productId: 'p2', name: 'Delta', price: 15, createdAt: '2026-01-04' },
    ];

    beforeEach(async () => {
      for (const d of docs) await collection.create(d);
    });

    it('returns all without query', async () => {
      const results = await collection.findMany();
      expect(results).toHaveLength(4);
    });

    it('filters by exact match', async () => {
      const results = await collection.findMany({ filter: { productId: 'p1' } });
      expect(results).toHaveLength(3);
    });

    it('filters with $gt', async () => {
      const results = await collection.findMany({ filter: { price: { $gt: 15 } } });
      expect(results).toHaveLength(2);
    });

    it('filters with $in', async () => {
      const results = await collection.findMany({ filter: { name: { $in: ['Alpha', 'Gamma'] } } });
      expect(results).toHaveLength(2);
    });

    it('sorts ascending', async () => {
      const results = await collection.findMany({ sort: { price: 1 } });
      expect(results.map(d => d.price)).toEqual([10, 15, 20, 30]);
    });

    it('sorts descending', async () => {
      const results = await collection.findMany({ sort: { price: -1 } });
      expect(results.map(d => d.price)).toEqual([30, 20, 15, 10]);
    });

    it('limits results', async () => {
      const results = await collection.findMany({ limit: 2 });
      expect(results).toHaveLength(2);
    });

    it('offsets + limits', async () => {
      const all = await collection.findMany({ sort: { price: 1 } });
      const page2 = await collection.findMany({ sort: { price: 1 }, offset: 2, limit: 2 });
      expect(page2).toEqual(all.slice(2, 4));
    });

    it('selects specific fields', async () => {
      const results = await collection.findMany({ select: ['id', 'name'] });
      expect(results[0]).toHaveProperty('id');
      expect(results[0]).toHaveProperty('name');
      expect(results[0]).not.toHaveProperty('price');
    });
  });

  describe('findOne', () => {
    it('returns first match', async () => {
      await collection.create({ id: '1', productId: 'p', name: 'A' });
      await collection.create({ id: '2', productId: 'p', name: 'B' });
      const result = await collection.findOne({ filter: { productId: 'p' } });
      expect(result).not.toBeNull();
    });

    it('returns null when no match', async () => {
      const result = await collection.findOne({ filter: { productId: 'missing' } });
      expect(result).toBeNull();
    });
  });

  describe('count', () => {
    it('counts all without filter', async () => {
      await collection.create({ id: '1', productId: 'p', name: 'A' });
      await collection.create({ id: '2', productId: 'p', name: 'B' });
      expect(await collection.count()).toBe(2);
    });

    it('counts with filter', async () => {
      await collection.create({ id: '1', productId: 'p1', name: 'A' });
      await collection.create({ id: '2', productId: 'p2', name: 'B' });
      expect(await collection.count({ productId: 'p1' })).toBe(1);
    });
  });

  describe('aggregate', () => {
    it('groups and aggregates', async () => {
      await collection.create({ id: '1', productId: 'p1', name: 'A', price: 10 });
      await collection.create({ id: '2', productId: 'p1', name: 'B', price: 20 });
      await collection.create({ id: '3', productId: 'p2', name: 'C', price: 30 });

      const results = await collection.aggregate<{ productId: string; total: number; cnt: number }>(
        {
          groupBy: 'productId',
          aggregations: [
            { field: 'price', op: 'sum', alias: 'total' },
            { field: 'price', op: 'count', alias: 'cnt' },
          ],
        }
      );

      expect(results).toHaveLength(2);
      const p1 = results.find(r => r.productId === 'p1');
      expect(p1?.total).toBe(30);
      expect(p1?.cnt).toBe(2);
    });
  });

  describe('rawQuery', () => {
    it('throws for memory provider', async () => {
      await expect(collection.rawQuery('SELECT * FROM c')).rejects.toThrow('not supported');
    });
  });

  describe('updateIfMatch (optimistic concurrency)', () => {
    interface RevDoc extends BaseDocument {
      name: string;
      rev?: number;
    }
    let revCol: DocumentCollection<RevDoc>;
    beforeEach(async () => {
      revCol = provider.getCollection<RevDoc>('rev', '/productId');
      await revCol.create({ id: '1', productId: 'p', name: 'A', rev: 0 });
    });

    it('matching rev writes the patch and bumps rev + _etag', async () => {
      const res = await revCol.updateIfMatch('1', 'p', { rev: 0 }, { name: 'B' });
      expect(res.ok).toBe(true);
      if (res.ok) {
        expect(res.doc.name).toBe('B');
        expect(res.doc.rev).toBe(1);
        expect(res.doc._etag).toBeDefined();
      }
    });

    it('stale rev => conflict and NO write', async () => {
      await revCol.updateIfMatch('1', 'p', { rev: 0 }, { name: 'B' }); // rev -> 1
      const res = await revCol.updateIfMatch('1', 'p', { rev: 0 }, { name: 'C' });
      expect(res.ok).toBe(false);
      if (!res.ok) expect(res.reason).toBe('conflict');
      // value unchanged by the losing write
      expect((await revCol.findById('1', 'p'))?.name).toBe('B');
    });

    it('matching etag writes; stale etag => conflict', async () => {
      const first = await revCol.updateIfMatch('1', 'p', { rev: 0 }, { name: 'B' });
      const goodEtag = first.ok ? first.doc._etag : undefined;
      const ok = await revCol.updateIfMatch('1', 'p', { etag: goodEtag }, { name: 'C' });
      expect(ok.ok).toBe(true);
      const stale = await revCol.updateIfMatch('1', 'p', { etag: goodEtag }, { name: 'D' });
      expect(stale.ok).toBe(false);
      if (!stale.ok) expect(stale.reason).toBe('conflict');
    });

    it('missing doc => not_found', async () => {
      const res = await revCol.updateIfMatch('nope', 'p', { rev: 0 }, { name: 'X' });
      expect(res.ok).toBe(false);
      if (!res.ok) expect(res.reason).toBe('not_found');
    });

    // THE PROOF: N concurrent compare-and-set on the SAME version — exactly one wins.
    it('is atomic under TRUE concurrency: N concurrent claims => exactly one winner', async () => {
      const N = 25;
      const results = await Promise.all(
        Array.from({ length: N }, (_, i) =>
          revCol.updateIfMatch('1', 'p', { rev: 0 }, { name: `claimer-${i}` })
        )
      );
      const winners = results.filter(r => r.ok);
      const conflicts = results.filter(r => !r.ok);
      expect(winners).toHaveLength(1);
      expect(conflicts).toHaveLength(N - 1);
      expect(conflicts.every(r => !r.ok && r.reason === 'conflict')).toBe(true);
      // the stored doc reflects exactly one applied write at rev 1
      const stored = await revCol.findById('1', 'p');
      expect(stored?.rev).toBe(1);
    });
  });
});

describe('matchesFilter', () => {
  const doc = { id: '1', productId: 'p', name: 'test', price: 25, tags: ['a', 'b'] };

  it('exact match', () => {
    expect(matchesFilter(doc, { name: 'test' })).toBe(true);
    expect(matchesFilter(doc, { name: 'other' })).toBe(false);
  });

  it('$ne', () => {
    expect(matchesFilter(doc, { price: { $ne: 30 } })).toBe(true);
    expect(matchesFilter(doc, { price: { $ne: 25 } })).toBe(false);
  });

  it('$exists', () => {
    expect(matchesFilter(doc, { price: { $exists: true } })).toBe(true);
    expect(matchesFilter(doc, { missing: { $exists: false } })).toBe(true);
    expect(matchesFilter(doc, { price: { $exists: false } })).toBe(false);
  });

  it('$startsWith', () => {
    expect(matchesFilter(doc, { name: { $startsWith: 'te' } })).toBe(true);
    expect(matchesFilter(doc, { name: { $startsWith: 'no' } })).toBe(false);
  });

  it('$contains on string', () => {
    expect(matchesFilter(doc, { name: { $contains: 'es' } })).toBe(true);
  });

  it('$contains on array', () => {
    expect(matchesFilter(doc, { tags: { $contains: 'a' } })).toBe(true);
    expect(matchesFilter(doc, { tags: { $contains: 'z' } })).toBe(false);
  });

  it('$or', () => {
    expect(matchesFilter(doc, { $or: [{ name: 'test' }, { name: 'other' }] })).toBe(true);
    expect(matchesFilter(doc, { $or: [{ name: 'x' }, { name: 'y' }] })).toBe(false);
  });

  it('combined conditions', () => {
    expect(matchesFilter(doc, { productId: 'p', price: { $gte: 20, $lt: 30 } })).toBe(true);
    expect(matchesFilter(doc, { productId: 'p', price: { $gte: 30 } })).toBe(false);
  });
});

describe('filterToCosmosSQL', () => {
  it('exact match', () => {
    const result = filterToCosmosSQL({ name: 'test' });
    expect(result.query).toBe('WHERE c.name = @p0');
    expect(result.parameters).toEqual([{ name: '@p0', value: 'test' }]);
  });

  it('comparison operators', () => {
    const result = filterToCosmosSQL({ price: { $gte: 10, $lt: 50 } });
    expect(result.query).toContain('c.price >= @p0');
    expect(result.query).toContain('c.price < @p1');
  });

  it('$exists', () => {
    const result = filterToCosmosSQL({ deleted: { $exists: false } });
    expect(result.query).toContain('NOT IS_DEFINED(c.deleted)');
  });

  it('$in', () => {
    const result = filterToCosmosSQL({ status: { $in: ['active', 'pending'] } });
    expect(result.query).toContain('c.status IN');
    expect(result.parameters).toHaveLength(2);
  });

  it('$contains emits ARRAY_CONTAINS OR CONTAINS', () => {
    const result = filterToCosmosSQL({ tags: { $contains: 'urgent' } });
    expect(result.query).toContain('ARRAY_CONTAINS(c.tags, @p0)');
    expect(result.query).toContain('CONTAINS(c.tags, @p0)');
    expect(result.parameters).toEqual([{ name: '@p0', value: 'urgent' }]);
  });

  it('$or', () => {
    const result = filterToCosmosSQL({ $or: [{ a: 1 }, { b: 2 }] });
    expect(result.query).toContain('OR');
  });

  it('empty filter returns empty WHERE', () => {
    const result = filterToCosmosSQL({});
    expect(result.query).toBe('');
  });
});
