import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CachePrefix, CacheTTL } from '../cache.service';
import { Cacheable } from '../decorators/cacheable.decorator';

describe('Cacheable decorator', () => {
  let mockCacheService: { getOrSet: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockCacheService = {
      getOrSet: vi.fn(),
    };
  });

  it('should call cacheService.getOrSet with correct key and TTL', async () => {
    class TestHandler {
      cacheService = mockCacheService;

      @Cacheable({
        prefix: CachePrefix.LISTING,
        ttl: CacheTTL.LISTING_DETAIL,
        resource: 'listing',
        keyFrom: (query: unknown) => [(query as { id: string }).id],
      })
      async execute(query: { id: string }): Promise<string> {
        return `listing-${query.id}`;
      }
    }

    mockCacheService.getOrSet.mockResolvedValue('cached-result');
    const handler = new TestHandler();
    const result = await handler.execute({ id: 'abc123' });

    expect(result).toBe('cached-result');
    expect(mockCacheService.getOrSet).toHaveBeenCalledWith(
      'cache:listing:abc123',
      expect.any(Function),
      300,
      'listing',
    );
  });

  it('should use first argument as default key when keyFrom is not provided', async () => {
    class TestHandler {
      cacheService = mockCacheService;

      @Cacheable({
        prefix: CachePrefix.PLAN_LIST,
        ttl: CacheTTL.PLAN_LIST,
        resource: 'plan',
      })
      async execute(query: string): Promise<string> {
        return `plan-${query}`;
      }
    }

    mockCacheService.getOrSet.mockResolvedValue('cached-plan');
    const handler = new TestHandler();
    await handler.execute('PREMIUM');

    expect(mockCacheService.getOrSet).toHaveBeenCalledWith(
      'cache:plan:list:premium',
      expect.any(Function),
      3600,
      'plan',
    );
  });

  it('should execute the original method as the loader function', async () => {
    class TestHandler {
      cacheService = mockCacheService;

      @Cacheable({
        prefix: CachePrefix.LISTING,
        ttl: CacheTTL.LISTING_DETAIL,
        resource: 'listing',
        keyFrom: (query: unknown) => [(query as { id: string }).id],
      })
      async execute(_query: { id: string }): Promise<string> {
        return 'original-result';
      }
    }

    mockCacheService.getOrSet.mockImplementation(
      async (_key: string, loader: () => Promise<string>) => loader(),
    );
    const handler = new TestHandler();
    const result = await handler.execute({ id: 'xyz' });

    expect(result).toBe('original-result');
  });

  it('should handle multiple key parts from keyFrom', async () => {
    class TestHandler {
      cacheService = mockCacheService;

      @Cacheable({
        prefix: CachePrefix.SEARCH,
        ttl: CacheTTL.SEARCH_RESULTS,
        resource: 'search',
        keyFrom: (query: unknown) => {
          const q = query as { district: string; type: string };
          return [q.district, q.type];
        },
      })
      async execute(_query: { district: string; type: string }): Promise<unknown[]> {
        return [];
      }
    }

    mockCacheService.getOrSet.mockResolvedValue([]);
    const handler = new TestHandler();
    await handler.execute({ district: 'Quận 1', type: 'APARTMENT' });

    expect(mockCacheService.getOrSet).toHaveBeenCalledWith(
      'cache:search:quận_1:apartment',
      expect.any(Function),
      120,
      'search',
    );
  });
});
