/* eslint-disable require-await, import/first */
// Unit tests for libs/entitlement.ts — cross-channel entitlement check.
// Models are mocked; DB is not touched.

import { checkEntitlement, listEntitlements } from '../../src/libs/entitlement';

const mockCustomerFindOne: jest.Mock<any, any[]> = jest.fn();
const mockSubscriptionFindAll: jest.Mock<any, any[]> = jest.fn();
const mockCreditGrantFindAll: jest.Mock<any, any[]> = jest.fn();
const mockPaymentMethodFindByPk: jest.Mock<any, any[]> = jest.fn();
const mockPriceFindAll: jest.Mock<any, any[]> = jest.fn();

jest.mock('../../src/store/models', () => ({
  Customer: { findOne: (...args: any[]) => mockCustomerFindOne(...args) },
  Subscription: { findAll: (...args: any[]) => mockSubscriptionFindAll(...args) },
  SubscriptionItem: {},
  Price: { findAll: (...args: any[]) => mockPriceFindAll(...args) },
  Product: {},
  CreditGrant: { findAll: (...args: any[]) => mockCreditGrantFindAll(...args) },
  PaymentMethod: { findByPk: (...args: any[]) => mockPaymentMethodFindByPk(...args) },
}));

const makeSub = (overrides: Record<string, any> = {}) => ({
  id: 'sub_1',
  status: 'active',
  channel: 'app_store',
  current_period_end: Math.floor(Date.now() / 1000) + 30 * 86400,
  default_payment_method_id: 'pm_1',
  items: [{ price: { product_id: 'pro_monthly' } }],
  ...overrides,
});

const makeGrant = (overrides: Record<string, any> = {}) => ({
  id: 'cg_1',
  customer_id: 'cus_1',
  status: 'granted',
  remaining_amount: '100',
  expires_at: Math.floor(Date.now() / 1000) + 30 * 86400,
  metadata: { product_id: 'one_off_product', payment_method_id: 'pm_stripe' },
  ...overrides,
});

beforeEach(() => {
  mockCustomerFindOne.mockReset();
  mockSubscriptionFindAll.mockReset().mockResolvedValue([]);
  mockCreditGrantFindAll.mockReset().mockResolvedValue([]);
  mockPaymentMethodFindByPk.mockReset();
  mockPriceFindAll.mockReset().mockResolvedValue([]);
});

describe('checkEntitlement', () => {
  it('returns inactive when customer not found', async () => {
    mockCustomerFindOne.mockResolvedValue(null);
    const result = await checkEntitlement({ customer_did: 'did:unknown', product_id: 'pro_monthly' });
    expect(result).toEqual({
      active: false,
      channel: null,
      expires_at: null,
      subscription_id: null,
      source: null,
    });
    expect(mockSubscriptionFindAll).not.toHaveBeenCalled();
  });

  it('returns active result for active subscription', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([makeSub()]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'pro_monthly' });
    expect(result.active).toBe(true);
    expect(result.subscription_id).toBe('sub_1');
    expect(result.channel).toBe('app_store');
    expect(result.source).toBe('subscription');
  });

  it('returns inactive=true subscription_id set for past_due (covers grace period)', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([makeSub({ status: 'past_due' })]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'pro_monthly' });
    expect(result.active).toBe(false);
    expect(result.subscription_id).toBe('sub_1');
    expect(result.source).toBe('subscription');
  });

  it('picks active over trialing over paused when multiple subs cover the same product', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([
      makeSub({ id: 'sub_paused', status: 'paused' }),
      makeSub({ id: 'sub_active', status: 'active' }),
      makeSub({ id: 'sub_trial', status: 'trialing' }),
    ]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'pro_monthly' });
    expect(result.subscription_id).toBe('sub_active');
    expect(result.active).toBe(true);
  });

  it('breaks ties on equal status by latest current_period_end', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([
      makeSub({ id: 'sub_older', status: 'active', current_period_end: 1700000000 }),
      makeSub({ id: 'sub_newer', status: 'active', current_period_end: 1800000000 }),
    ]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'pro_monthly' });
    expect(result.subscription_id).toBe('sub_newer');
  });

  it('skips subscriptions whose items do not cover the target product', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([makeSub({ items: [{ price: { product_id: 'other_product' } }] })]);
    mockCreditGrantFindAll.mockResolvedValue([]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'pro_monthly' });
    expect(result.active).toBe(false);
    expect(result.subscription_id).toBeNull();
  });

  it('resolves IAP subscription live via Product channel mapping (no items, empty metadata.product_id)', async () => {
    // Mirrors the real app_store case: subscription has no SubscriptionItems and
    // an empty metadata.product_id (Product mapping was added after purchase),
    // but reliably stores the channel SKU + bundle_id in payment_details.app_store.
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([
      makeSub({
        id: 'sub_iap',
        channel: 'app_store',
        items: [],
        metadata: {},
        payment_details: { app_store: { product_id: 'pk_demo_monthly', bundle_id: 'com.example.app' } },
      }),
    ]);
    // Querying the local Product id; its Price.metadata maps to the App Store SKU
    // (Stripe-style — SKU binding lives on Price, not Product). Multi-tenant:
    // bundle_id is part of the lookup key so two apps with the same SKU string
    // don't collide.
    mockPriceFindAll.mockResolvedValue([
      { product_id: 'prod_X', metadata: { app_store_product_id: 'pk_demo_monthly', bundle_id: 'com.example.app' } },
    ]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'prod_X' });
    expect(result.active).toBe(true);
    expect(result.subscription_id).toBe('sub_iap');
    expect(result.source).toBe('subscription');
  });

  it('does NOT match when the Price channel SKU differs from the subscription SKU', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([
      makeSub({
        id: 'sub_iap',
        items: [],
        metadata: {},
        payment_details: { app_store: { product_id: 'pk_demo_monthly', bundle_id: 'com.example.app' } },
      }),
    ]);
    mockPriceFindAll.mockResolvedValue([
      { product_id: 'prod_Y', metadata: { app_store_product_id: 'pk_other_sku', bundle_id: 'com.example.app' } },
    ]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'prod_Y' });
    expect(result.active).toBe(false);
    expect(result.subscription_id).toBeNull();
  });

  it('IAP SKU rebind does not double-grant — live mapping wins over a stale SubscriptionItem', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    // app_store sub: stale item points to prod_A, but the stored channel SKU is 'sku1'.
    mockSubscriptionFindAll.mockResolvedValue([
      makeSub({
        channel: 'app_store',
        items: [{ price: { product_id: 'prod_A' } }],
        payment_details: { app_store: { product_id: 'sku1', bundle_id: 'com.example.app' } },
      }),
    ]);

    // Query prod_A: no Price for it bound to sku1 → must be inactive (stale
    // item ignored).
    mockPriceFindAll.mockResolvedValue([
      { product_id: 'prod_A', metadata: { app_store_product_id: 'other_sku', bundle_id: 'com.example.app' } },
    ]);
    const a = await checkEntitlement({ customer_did: 'did:1', product_id: 'prod_A' });
    expect(a.active).toBe(false);
    expect(a.subscription_id).toBeNull();

    // Query prod_B: a Price under prod_B is now bound to sku1 → active via
    // live mapping.
    mockPriceFindAll.mockResolvedValue([
      { product_id: 'prod_B', metadata: { app_store_product_id: 'sku1', bundle_id: 'com.example.app' } },
    ]);
    const b = await checkEntitlement({ customer_did: 'did:1', product_id: 'prod_B' });
    expect(b.active).toBe(true);
    expect(b.subscription_id).toBe('sub_1');
  });

  it('falls back to PaymentMethod.type when Subscription.channel is unset', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([makeSub({ channel: undefined })]);
    mockPaymentMethodFindByPk.mockResolvedValue({ type: 'stripe' });

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'pro_monthly' });
    expect(result.channel).toBe('stripe');
  });

  it('falls back to CreditGrant when no subscription matches', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([]);
    mockCreditGrantFindAll.mockResolvedValue([makeGrant({ metadata: { product_id: 'pro_monthly' } })]);
    mockPaymentMethodFindByPk.mockResolvedValue({ type: 'stripe' });

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'pro_monthly' });
    expect(result.active).toBe(true);
    expect(result.source).toBe('one_time');
    expect(result.credit_remaining).toBe('100');
  });

  it('CreditGrant with remaining_amount=0 is inactive', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([]);
    mockCreditGrantFindAll.mockResolvedValue([
      makeGrant({ metadata: { product_id: 'pro_monthly' }, remaining_amount: '0' }),
    ]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'pro_monthly' });
    expect(result.active).toBe(false);
    expect(result.source).toBe('one_time');
  });

  it('CreditGrant past expires_at is inactive', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([]);
    mockCreditGrantFindAll.mockResolvedValue([
      makeGrant({ metadata: { product_id: 'pro_monthly' }, expires_at: 1000 }),
    ]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'pro_monthly' });
    expect(result.active).toBe(false);
  });
});

describe('listEntitlements', () => {
  it('returns [] when customer not found', async () => {
    mockCustomerFindOne.mockResolvedValue(null);
    expect(await listEntitlements({ customer_did: 'did:unknown' })).toEqual([]);
  });

  it('returns one row per distinct product, picking best subscription', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([
      makeSub({
        id: 'sub_pro',
        status: 'active',
        items: [{ price: { product_id: 'pro_monthly' } }],
      }),
      makeSub({
        id: 'sub_paused_pro',
        status: 'paused',
        items: [{ price: { product_id: 'pro_monthly' } }],
      }),
      makeSub({
        id: 'sub_basic',
        status: 'trialing',
        items: [{ price: { product_id: 'basic' } }],
      }),
    ]);

    const list = await listEntitlements({ customer_did: 'did:1' });
    expect(list).toHaveLength(2);
    const pro = list.find((x) => x.product_id === 'pro_monthly');
    expect(pro?.subscription_id).toBe('sub_pro');
    const basic = list.find((x) => x.product_id === 'basic');
    expect(basic?.subscription_id).toBe('sub_basic');
  });

  it('merges CreditGrant entitlements for products not covered by subscriptions', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([
      makeSub({ id: 'sub_pro', items: [{ price: { product_id: 'pro_monthly' } }] }),
    ]);
    mockCreditGrantFindAll.mockResolvedValue([
      makeGrant({ metadata: { product_id: 'one_off' } }),
      // Already covered by subscription — should NOT duplicate
      makeGrant({ id: 'cg_dup', metadata: { product_id: 'pro_monthly' } }),
    ]);
    mockPaymentMethodFindByPk.mockResolvedValue({ type: 'stripe' });

    const list = await listEntitlements({ customer_did: 'did:1' });
    expect(list).toHaveLength(2);
    const oneOff = list.find((x) => x.source === 'one_time');
    expect(oneOff?.product_id).toBe('one_off');
    expect(oneOff?.active).toBe(true);
  });

  it('lists an IAP subscription under its live-mapped product, not a stale item', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([
      makeSub({
        channel: 'app_store',
        items: [{ price: { product_id: 'prod_A_stale' } }],
        payment_details: { app_store: { product_id: 'sku1', bundle_id: 'com.example.app' } },
      }),
    ]);
    // Live catalogue: a Price under prod_B is bound to sku1.
    mockPriceFindAll.mockResolvedValue([
      { product_id: 'prod_B', metadata: { app_store_product_id: 'sku1', bundle_id: 'com.example.app' } },
    ]);

    const list = await listEntitlements({ customer_did: 'did:1' });
    expect(list).toHaveLength(1);
    expect(list[0]!.product_id).toBe('prod_B');
  });

  it('multi-tenant: same SKU string across two apps does not grant the wrong product (entitlement leak)', async () => {
    // Two iOS apps, both legitimately using SKU 'pro_monthly' in their own
    // App Store Connect namespaces (Apple SKUs are per-bundle-id, not global).
    // App A's customer has a sub for App A's pro_monthly; App B has its own
    // distinct Product/Price. Without (sku, bundle_id) scoping in entitlement.ts,
    // App A's sub would incorrectly grant App B's product too.
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_appA_user' });
    mockSubscriptionFindAll.mockResolvedValue([
      makeSub({
        channel: 'app_store',
        items: [],
        metadata: {},
        payment_details: { app_store: { product_id: 'pro_monthly', bundle_id: 'com.appA' } },
      }),
    ]);
    // Querying App B's Product, whose Price uses the same SKU string under a
    // different bundle_id — must NOT match App A's sub.
    mockPriceFindAll.mockResolvedValue([
      { product_id: 'prod_appB_pro', metadata: { app_store_product_id: 'pro_monthly', bundle_id: 'com.appB' } },
    ]);

    const result = await checkEntitlement({ customer_did: 'did:1', product_id: 'prod_appB_pro' });
    expect(result.active).toBe(false);
    expect(result.subscription_id).toBeNull();
  });

  it('skips CreditGrants without product_id metadata', async () => {
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1' });
    mockSubscriptionFindAll.mockResolvedValue([]);
    mockCreditGrantFindAll.mockResolvedValue([makeGrant({ metadata: {} })]);

    expect(await listEntitlements({ customer_did: 'did:1' })).toEqual([]);
  });
});
