// Mock firebase-admin before any module under test is imported.
// `admin.firestore` must work as both a callable (admin.firestore()) and a
// namespace (admin.firestore.Timestamp). Object.assign achieves both.
jest.mock('firebase-admin', () => {
  const firestoreFn: any = jest.fn();
  firestoreFn.Timestamp = {
    now: jest.fn(() => ({ toMillis: () => 1_000_000_000_000 })),
    fromMillis: jest.fn((ms: number) => ({ toMillis: () => ms })),
  };
  return {
    firestore: firestoreFn,
    apps: [] as any[],
    initializeApp: jest.fn(),
  };
});

import * as admin from 'firebase-admin';
import {
  isPremiumEntitlement,
  PREMIUM_ACTIVE_TYPES,
  PREMIUM_REVOKED_TYPES,
  applyEntitlementEvent,
  EntitlementEvent,
} from './entitlementLogic';

// ── Helpers ───────────────────────────────────────────────────────────────────

function event(overrides: Partial<EntitlementEvent> = {}): EntitlementEvent {
  return {
    id: 'evt-001',
    type: 'INITIAL_PURCHASE',
    app_user_id: 'user-123',
    product_id: 'closer_premium_monthly',
    entitlement_id: 'closer_premium',
    ...overrides,
  };
}

function buildFirestoreMock(opts: { createShouldFail?: boolean } = {}) {
  const mockSet = jest.fn().mockResolvedValue(undefined);
  const mockCreate = jest.fn().mockImplementation(() =>
    opts.createShouldFail
      ? Promise.reject(Object.assign(new Error('ALREADY_EXISTS'), { code: 6 }))
      : Promise.resolve(undefined)
  );

  // Leaf doc ref (e.g. users/{uid}/entitlements/premium)
  const deepDocRef = { set: mockSet };
  const deepDocFn = jest.fn().mockReturnValue(deepDocRef);
  const subCollectionRef = { doc: deepDocFn };
  const subCollectionFn = jest.fn().mockReturnValue(subCollectionRef);

  // Top-level doc ref: supports create (idempotency marker) and subcollection access
  const topDocRef = { create: mockCreate, set: mockSet, collection: subCollectionFn };
  const mockDoc = jest.fn().mockReturnValue(topDocRef);
  const mockCollection = jest.fn().mockReturnValue({ doc: mockDoc });
  const mockInstance = { collection: mockCollection };

  (admin.firestore as unknown as jest.Mock).mockReturnValue(mockInstance);

  return { mockSet, mockCreate };
}

// ── isPremiumEntitlement ──────────────────────────────────────────────────────

describe('isPremiumEntitlement', () => {
  it('returns true when entitlement_id matches', () => {
    expect(isPremiumEntitlement(event({ entitlement_id: 'closer_premium' }))).toBe(true);
  });

  it('returns true when entitlement_ids array includes the id', () => {
    expect(
      isPremiumEntitlement(event({ entitlement_id: undefined, entitlement_ids: ['closer_premium', 'other'] }))
    ).toBe(true);
  });

  it('returns false for a different entitlement_id', () => {
    expect(
      isPremiumEntitlement(event({ entitlement_id: 'other_entitlement', entitlement_ids: [] }))
    ).toBe(false);
  });

  it('returns false when neither id nor ids reference the premium entitlement', () => {
    expect(
      isPremiumEntitlement(event({ entitlement_id: undefined, entitlement_ids: ['not_premium'] }))
    ).toBe(false);
  });
});

// ── Event type sets ───────────────────────────────────────────────────────────

describe('PREMIUM_ACTIVE_TYPES', () => {
  it.each(['INITIAL_PURCHASE', 'RENEWAL', 'PRODUCT_CHANGE', 'TRANSFER', 'UNCANCELLATION'] as const)(
    'contains %s',
    (type) => expect(PREMIUM_ACTIVE_TYPES.has(type)).toBe(true)
  );

  it.each(['EXPIRATION', 'CANCELLATION', 'BILLING_ISSUE'] as const)(
    'does not contain revocation event %s',
    (type) => expect(PREMIUM_ACTIVE_TYPES.has(type)).toBe(false)
  );
});

describe('PREMIUM_REVOKED_TYPES', () => {
  it.each(['EXPIRATION', 'CANCELLATION', 'BILLING_ISSUE', 'SUBSCRIBER_ALIAS'] as const)(
    'contains %s',
    (type) => expect(PREMIUM_REVOKED_TYPES.has(type)).toBe(true)
  );

  it.each(['INITIAL_PURCHASE', 'RENEWAL'] as const)(
    'does not contain active event %s',
    (type) => expect(PREMIUM_REVOKED_TYPES.has(type)).toBe(false)
  );
});

// ── applyEntitlementEvent ─────────────────────────────────────────────────────

describe('applyEntitlementEvent', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('grants premium on INITIAL_PURCHASE', async () => {
    const { mockSet } = buildFirestoreMock();

    await applyEntitlementEvent(event({ type: 'INITIAL_PURCHASE' }));

    expect(mockSet).toHaveBeenCalledWith(
      expect.objectContaining({ premium: true }),
    );
  });

  it('revokes premium on EXPIRATION', async () => {
    const { mockSet } = buildFirestoreMock();

    await applyEntitlementEvent(event({ type: 'EXPIRATION' }));

    expect(mockSet).toHaveBeenCalledWith(
      expect.objectContaining({ premium: false, expiresAt: null }),
    );
  });

  it('revokes premium on CANCELLATION', async () => {
    const { mockSet } = buildFirestoreMock();

    await applyEntitlementEvent(event({ type: 'CANCELLATION' }));

    expect(mockSet).toHaveBeenCalledWith(
      expect.objectContaining({ premium: false }),
    );
  });

  it('is idempotent — skips duplicate event ids', async () => {
    const { mockSet } = buildFirestoreMock({ createShouldFail: true });

    await applyEntitlementEvent(event());

    // The idempotency guard fires (create threw ALREADY_EXISTS), so set is never called.
    expect(mockSet).not.toHaveBeenCalled();
  });

  it('stores expiresAt when expiration_at_ms is present', async () => {
    const { mockSet } = buildFirestoreMock();
    const expiresAtMs = Date.now() + 86_400_000;

    await applyEntitlementEvent(event({ type: 'RENEWAL', expiration_at_ms: expiresAtMs }));

    expect(mockSet).toHaveBeenCalledWith(
      expect.objectContaining({ premium: true }),
    );
  });

  it('ignores non-premium entitlement events', async () => {
    const { mockSet } = buildFirestoreMock();

    await applyEntitlementEvent(
      event({ entitlement_id: 'some_other_entitlement', entitlement_ids: [] })
    );

    expect(mockSet).not.toHaveBeenCalled();
  });
});
