/**
 * Tests for products in-memory cache.
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';

vi.mock('./repository.js', () => ({
  getAll: vi.fn(),
}));

import { getAll } from './repository.js';
import {
  loadProductCache,
  getProduct,
  isValidProduct,
  getAllProducts,
  productsForUser,
  cacheSize,
} from './cache.js';

const mockGetAll = vi.mocked(getAll);

const products = [
  {
    id: 'lysnrai',
    productId: 'lysnrai',
    displayName: 'LysnrAI',
    licensePrefix: 'LYSNR',
    packageName: 'com.bytelyst.lysnrai',
    defaultPlan: 'free' as const,
    trialDays: 14,
    deviceLimits: { free: 1, pro: 3, enterprise: 10 },
    websiteUrl: 'https://lysnr.ai',
    status: 'active' as const,
    createdAt: '2026-01-01T00:00:00Z',
    updatedAt: '2026-01-01T00:00:00Z',
  },
  {
    id: 'mindlyst',
    productId: 'mindlyst',
    displayName: 'MindLyst',
    licensePrefix: 'MIND',
    packageName: 'com.bytelyst.mindlyst',
    defaultPlan: 'free' as const,
    trialDays: 7,
    deviceLimits: { free: 1, pro: 3, enterprise: 10 },
    websiteUrl: 'https://mindlyst.com',
    status: 'active' as const,
    createdAt: '2026-01-01T00:00:00Z',
    updatedAt: '2026-01-01T00:00:00Z',
  },
];

describe('product cache', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('loadProductCache', () => {
    it('loads products into cache', async () => {
      mockGetAll.mockResolvedValue(products);
      await loadProductCache();
      expect(cacheSize()).toBe(2);
    });

    it('clears old cache on reload', async () => {
      mockGetAll.mockResolvedValue(products);
      await loadProductCache();
      expect(cacheSize()).toBe(2);

      mockGetAll.mockResolvedValue([products[0]]);
      await loadProductCache();
      expect(cacheSize()).toBe(1);
    });
  });

  describe('getProduct', () => {
    it('returns product from cache', async () => {
      mockGetAll.mockResolvedValue(products);
      await loadProductCache();
      const p = getProduct('lysnrai');
      expect(p?.displayName).toBe('LysnrAI');
    });

    it('returns undefined for unknown product', async () => {
      mockGetAll.mockResolvedValue(products);
      await loadProductCache();
      expect(getProduct('unknown')).toBeUndefined();
    });
  });

  describe('isValidProduct', () => {
    it('returns true for cached product', async () => {
      mockGetAll.mockResolvedValue(products);
      await loadProductCache();
      expect(isValidProduct('lysnrai')).toBe(true);
    });

    it('returns false for unknown product', async () => {
      mockGetAll.mockResolvedValue(products);
      await loadProductCache();
      expect(isValidProduct('unknown')).toBe(false);
    });
  });

  describe('getAllProducts', () => {
    it('returns all cached products (plus temporary fallback products)', async () => {
      mockGetAll.mockResolvedValue(products);
      await loadProductCache();
      const all = getAllProducts();
      // The cache holds 2 products (lysnrai, mindlyst) and getAllProducts()
      // additionally injects entries from TEMPORARY_FALLBACK_PRODUCTS that
      // are not already in the cache. Today that set is { invttrdg }, so
      // the total is 3. When the invttrdg fallback is removed (see
      // cache.ts TODO), this expectation should drop back to 2.
      expect(all).toHaveLength(3);
      expect(all.map(p => p.id)).toContain('lysnrai');
      expect(all.map(p => p.id)).toContain('mindlyst');
      expect(all.map(p => p.id)).toContain('invttrdg');
    });
  });

  describe('productsForUser (owner-scoped "my projects")', () => {
    const owned = [
      { ...products[0], ownerId: 'user_a' },
      { ...products[1], ownerId: 'user_b' },
    ];

    it('a normal user sees only the products they own', async () => {
      mockGetAll.mockResolvedValue(owned);
      await loadProductCache();
      const mine = productsForUser('user_a', false);
      expect(mine.map(p => p.id)).toEqual(['lysnrai']);
    });

    it('owner-less / other-owner products are hidden from a normal user', async () => {
      mockGetAll.mockResolvedValue(owned);
      await loadProductCache();
      // invttrdg (the fallback) has no ownerId ⇒ never shown to a non-admin.
      expect(productsForUser('user_a', false).map(p => p.id)).not.toContain('invttrdg');
      expect(productsForUser('nobody', false)).toEqual([]);
    });

    it('an admin sees every product regardless of owner', async () => {
      mockGetAll.mockResolvedValue(owned);
      await loadProductCache();
      const all = productsForUser('anyone', true);
      expect(all.map(p => p.id)).toEqual(
        expect.arrayContaining(['lysnrai', 'mindlyst', 'invttrdg'])
      );
    });
  });

  describe('cacheSize', () => {
    it('returns 0 before loading', async () => {
      mockGetAll.mockResolvedValue([]);
      await loadProductCache();
      expect(cacheSize()).toBe(0);
    });
  });
});
