import { describe, it, expect, beforeEach } from 'bun:test';
import { queryJsonArray, queryJsonObject } from './json-query';
import * as path from 'path';

// Test data interfaces
interface TestUser {
  id: number;
  name: string;
  age: number;
  department: string;
}

interface TestKeyValue {
  key: string;
  value: any;
}

// Test file paths
const basePath = path.join(__dirname, '../fixtures/jsonQuery');
const testArrayFile = path.join(basePath, 'test-data-array.json');
const testObjectFile = path.join(basePath, 'test-data-object.json');
const testInvalidFile = path.join(basePath, 'test-data-invalid.json');
const nonExistentFile = path.join(basePath, 'non-existent.json');

describe('queryJsonArray', () => {
  beforeEach(() => {
    // Clear any existing cache before each test
    const NodeCache = require('node-cache');
    const cache = new NodeCache();
    cache.flushAll();
  });

  it('should find a matching item in JSON array', async () => {
    const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Alice');

    expect(result).not.toBeNull();
    expect(result?.name).toBe('Alice');
    expect(result?.id).toBe(1);
    expect(result?.age).toBe(30);
    expect(result?.department).toBe('Engineering');
  });

  it('should find the first matching item when multiple matches exist', async () => {
    const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.department === 'Engineering');

    expect(result).not.toBeNull();
    expect(result?.name).toBe('Alice'); // First engineering employee
    expect(result?.id).toBe(1);
  });

  it('should return null when no match is found', async () => {
    const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'NonExistent');

    expect(result).toBeNull();
  });

  it('should handle complex query conditions', async () => {
    const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.age > 30 && user.department === 'Engineering');

    expect(result).not.toBeNull();
    expect(result?.name).toBe('Charlie');
    expect(result?.age).toBe(35);
  });

  it('should cache results when cacheKey is provided', async () => {
    const cacheKey = 'test-alice-query';

    // First call should hit the file
    const result1 = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Alice', cacheKey);

    // Second call should hit the cache (we can't directly verify this without mocking,
    // but we can verify the result is consistent)
    const result2 = await queryJsonArray<TestUser>(
      testArrayFile,
      (user) => user.name === 'Bob', // Different query, but should return cached Alice
      cacheKey,
    );

    expect(result1).toEqual(result2);
    expect(result1?.name).toBe('Alice');
    expect(result2?.name).toBe('Alice');
  });

  it('should respect custom cache expiry', async () => {
    const cacheKey = 'test-expiry-query';
    const customExpiry = 1; // 1 second

    const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Bob', cacheKey, customExpiry);

    expect(result).not.toBeNull();
    expect(result?.name).toBe('Bob');
  });

  it('should handle file read errors gracefully', async () => {
    await expect(queryJsonArray<TestUser>(nonExistentFile, (user) => user.name === 'Alice')).rejects.toThrow();
  });

  it('should handle invalid JSON gracefully', async () => {
    await expect(queryJsonArray<TestUser>(testInvalidFile, (user) => user.name === 'Alice')).rejects.toThrow();
  });

  it('should work with numeric queries', async () => {
    const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.id === 3);

    expect(result).not.toBeNull();
    expect(result?.name).toBe('Charlie');
    expect(result?.id).toBe(3);
  });

  it('should work with range queries', async () => {
    const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.age >= 30 && user.age <= 32);

    expect(result).not.toBeNull();
    expect(result?.name).toBe('Alice'); // First match: age 30
  });

  it('should handle empty query results', async () => {
    const result = await queryJsonArray<TestUser>(
      testArrayFile,
      (user) => user.age > 100, // No one is over 100
    );

    expect(result).toBeNull();
  });
});

describe('queryJsonObject', () => {
  beforeEach(() => {
    // Clear any existing cache before each test
    const NodeCache = require('node-cache');
    const cache = new NodeCache();
    cache.flushAll();
  });

  it('should find a matching key-value pair in JSON object', async () => {
    const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'users');

    expect(result).not.toBeNull();
    expect(typeof result).toBe('object');
    expect(result?.alice?.name).toBe('Alice');
    expect(result?.bob?.name).toBe('Bob');
  });

  it('should find nested object values', async () => {
    const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'departments');

    expect(result).not.toBeNull();
    expect(result?.engineering?.name).toBe('Engineering');
    expect(result?.marketing?.budget).toBe(500000);
  });

  it('should find specific configuration values', async () => {
    const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'config');

    expect(result).not.toBeNull();
    expect(result?.version).toBe('1.0.0');
    expect(result?.environment).toBe('test');
  });

  it('should return null when no match is found', async () => {
    const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'nonexistent');

    expect(result).toBeNull();
  });

  it('should handle complex query conditions on values', async () => {
    const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => {
      if (item.key === 'departments' && typeof item.value === 'object') {
        return Object.values(item.value).some((dept: any) => dept.budget > 800000);
      }
      return false;
    });

    expect(result).not.toBeNull();
    expect(result?.engineering?.budget).toBe(1000000);
  });

  it('should cache results when cacheKey is provided', async () => {
    const cacheKey = 'test-config-query';

    // First call should hit the file
    const result1 = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'config', cacheKey);

    // Second call should hit the cache
    const result2 = await queryJsonObject<any, TestKeyValue>(
      testObjectFile,
      (item) => item.key === 'users', // Different query, but should return cached config
      cacheKey,
    );

    expect(result1).toEqual(result2);
    expect(result1?.version).toBe('1.0.0');
    expect(result2?.version).toBe('1.0.0');
  });

  it('should respect custom cache expiry', async () => {
    const cacheKey = 'test-object-expiry-query';
    const customExpiry = 2; // 2 seconds

    const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => item.key === 'users', cacheKey, customExpiry);

    expect(result).not.toBeNull();
    expect(result?.alice?.name).toBe('Alice');
  });

  it('should handle file read errors gracefully', async () => {
    await expect(queryJsonObject<any, TestKeyValue>(nonExistentFile, (item) => item.key === 'config')).rejects.toThrow();
  });

  it('should handle invalid JSON gracefully', async () => {
    await expect(queryJsonObject<any, TestKeyValue>(testInvalidFile, (item) => item.key === 'config')).rejects.toThrow();
  });

  it('should work with value-based queries', async () => {
    const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => {
      return typeof item.value === 'object' && item.value?.version === '1.0.0';
    });

    expect(result).not.toBeNull();
    expect(result?.version).toBe('1.0.0');
    expect(result?.environment).toBe('test');
  });

  it('should handle queries that check both key and value', async () => {
    const result = await queryJsonObject<any, TestKeyValue>(testObjectFile, (item) => {
      return item.key.startsWith('dep') && typeof item.value === 'object';
    });

    expect(result).not.toBeNull();
    expect(result?.engineering?.name).toBe('Engineering');
  });
});

describe('Edge cases and error handling', () => {
  it('should handle empty file paths', async () => {
    await expect(queryJsonArray<any>('', () => true)).rejects.toThrow();
  });

  it('should handle null query functions gracefully', async () => {
    await expect(queryJsonArray<TestUser>(testArrayFile, null as any)).rejects.toThrow();
  });

  it('should handle undefined query functions gracefully', async () => {
    await expect(queryJsonArray<TestUser>(testArrayFile, undefined as any)).rejects.toThrow();
  });

  it('should work without caching parameters', async () => {
    const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Diana');

    expect(result).not.toBeNull();
    expect(result?.name).toBe('Diana');
    expect(result?.department).toBe('Sales');
  });

  it('should work with minimal cache configuration', async () => {
    const result = await queryJsonArray<TestUser>(testArrayFile, (user) => user.name === 'Eve', 'minimal-cache');

    expect(result).not.toBeNull();
    expect(result?.name).toBe('Eve');
    expect(result?.age).toBe(32);
  });
});
