/* eslint-disable require-await, import/first */
// Unit tests for ingestVerifiedAppStorePurchase.
// Models / audit / client are fully mocked — DB is not touched.

process.env.APP_STORE_SKIP_SIGNATURE_VERIFY = 'true';

import { ingestVerifiedAppStorePurchase } from '../../../src/integrations/app-store/handlers/subscription';
import type { AppStoreClient, AppStoreTransactionPayload } from '../../../src/integrations/app-store/client';
import type { PaymentMethod } from '../../../src/store/models';

const mockSubscriptionFindOne: jest.Mock<any, any[]> = jest.fn();
const mockSubscriptionCount: jest.Mock<any, any[]> = jest.fn();
const mockSubscriptionCreate: jest.Mock<any, any[]> = jest.fn();
const mockSubscriptionItemCreate: jest.Mock<any, any[]> = jest.fn();
const mockCustomerFindOrCreate: jest.Mock<any, any[]> = jest.fn();
const mockCustomerFindByPk: jest.Mock<any, any[]> = jest.fn();
const mockPriceFindOne: jest.Mock<any, any[]> = jest.fn();
const mockGetInvoicePrefix = jest.fn(() => 'TEST');
const mockCreateEvent: jest.Mock<Promise<undefined>, any[]> = jest.fn(async () => undefined);

jest.mock('../../../src/store/models', () => ({
  Subscription: {
    findOne: (...args: any[]) => mockSubscriptionFindOne(...args),
    count: (...args: any[]) => mockSubscriptionCount(...args),
    create: (...args: any[]) => mockSubscriptionCreate(...args),
  },
  Customer: {
    findOrCreate: (...args: any[]) => mockCustomerFindOrCreate(...args),
    findByPk: (...args: any[]) => mockCustomerFindByPk(...args),
    getInvoicePrefix: () => mockGetInvoicePrefix(),
  },
  SubscriptionItem: { create: (...args: any[]) => mockSubscriptionItemCreate(...args) },
  Price: { findOne: (...args: any[]) => mockPriceFindOne(...args) },
  PaymentMethod: {},
}));

jest.mock('../../../src/libs/audit', () => ({
  createEvent: (...args: any[]) => mockCreateEvent(...args),
}));

const makeClient = (payload: Partial<AppStoreTransactionPayload>): AppStoreClient => {
  const buildPayload = () =>
    ({
      transactionId: 'txn_1',
      originalTransactionId: 'orig_1',
      productId: 'pro_monthly',
      bundleId: 'com.example.app',
      environment: 'Sandbox',
      expiresDate: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30 days out
      purchaseDate: Date.now() - 60_000,
      appAccountToken: 'uuid-1',
      ...payload,
    }) as AppStoreTransactionPayload;
  return {
    bundleId: 'com.example.app',
    environment: 'sandbox',
    verifyJwsTransaction: jest.fn(async () => buildPayload()),
    verifyLegacyReceipt: jest.fn(async () => buildPayload()),
  } as unknown as AppStoreClient;
};

const makeMethod = (overrides: Record<string, any> = {}): PaymentMethod =>
  ({
    id: 'pm_app_store_1',
    livemode: false,
    default_currency_id: 'cur_1',
    ...overrides,
  }) as unknown as PaymentMethod;

beforeEach(() => {
  mockSubscriptionFindOne.mockReset();
  mockSubscriptionCount.mockReset().mockResolvedValue(0);
  mockSubscriptionCreate.mockReset();
  mockSubscriptionItemCreate.mockReset();
  mockCustomerFindOrCreate.mockReset();
  // Default: any existing sub is owned by the same DID the tests verify under
  // (did:abc) → ownership guard passes. Mismatch tests override this.
  mockCustomerFindByPk.mockReset().mockResolvedValue({ id: 'cus_1', did: 'did:abc' });
  mockPriceFindOne.mockReset().mockResolvedValue(null);
  mockCreateEvent.mockReset().mockImplementation(async () => undefined);
});

describe('ingestVerifiedAppStorePurchase', () => {
  it('is idempotent — returns existing subscription if originalTransactionId already mapped', async () => {
    const existing = { id: 'sub_existing', status: 'active', customer_id: 'cus_1', update: jest.fn() };
    mockSubscriptionFindOne.mockResolvedValue(existing);
    const client = makeClient({});

    const result = await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client,
      signedTransaction: 'jws-string',
    });

    expect(result.subscription).toBe(existing);
    expect(result.isFirstSubscribe).toBe(false);
    expect(mockSubscriptionCreate).not.toHaveBeenCalled();
    // Same owner → no re-attach write on the subscription.
    expect(existing.update).not.toHaveBeenCalled();
  });

  it('refuses verify when the Apple subscription belongs to a different DID (no migration)', async () => {
    // The Apple subscription (same originalTransactionId) is owned by another DID.
    // A different DID verifies it → refuse with an ownership-mismatch error;
    // the subscription is NOT migrated and NOT mutated. Mirrors OpenAI/Claude.
    const update = jest.fn();
    const existing = {
      id: 'sub_existing',
      status: 'active',
      customer_id: 'cus_OLD',
      payment_details: { app_store: { original_transaction_id: 'orig_1', product_id: 'pro_monthly' } },
      update,
    };
    mockSubscriptionFindOne.mockResolvedValue(existing);
    mockCustomerFindByPk.mockResolvedValue({ id: 'cus_OLD', did: 'did:original' });

    await expect(
      ingestVerifiedAppStorePurchase({
        customerDid: 'did:new',
        paymentMethod: makeMethod(),
        client: makeClient({}),
        signedTransaction: 'jws-string',
      })
    ).rejects.toThrow(/different account/i);

    expect(update).not.toHaveBeenCalled();
    expect(mockSubscriptionCreate).not.toHaveBeenCalled();
  });

  it('reactivates a lapsed (canceled) subscription when a fresh valid transaction arrives', async () => {
    // Sandbox re-subscribe / renewal via client verify reuses the same
    // originalTransactionId. The stored sub had expired→canceled, but the new
    // transaction is valid → it must be revived to active, not returned stale.
    const update = jest.fn(async () => undefined);
    const existing = {
      id: 'sub_existing',
      status: 'canceled',
      customer_id: 'cus_1',
      payment_details: { app_store: { original_transaction_id: 'orig_1', product_id: 'pro_monthly' } },
      update,
    };
    mockSubscriptionFindOne.mockResolvedValue(existing);
    const future = Date.now() + 30 * 24 * 60 * 60 * 1000;
    const client = makeClient({ expiresDate: future, transactionId: 'txn_2' });

    const result = await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client,
      signedTransaction: 'jws-string',
    });

    expect(result.subscription).toBe(existing);
    expect(result.isFirstSubscribe).toBe(false);
    expect(update).toHaveBeenCalledWith(
      expect.objectContaining({
        status: 'active',
        current_period_end: Math.floor(future / 1000),
        cancel_at_period_end: false,
        payment_details: expect.objectContaining({
          app_store: expect.objectContaining({ transaction_id: 'txn_2', expires_at: Math.floor(future / 1000) }),
        }),
      })
    );
    expect(mockSubscriptionCreate).not.toHaveBeenCalled();
    expect(mockCreateEvent).toHaveBeenCalledWith(
      'Subscription',
      'customer.subscription.started',
      existing
    );
  });

  it('reactivation self-heals empty metadata.product_id from the SKU→Price mapping', async () => {
    // The Price↔SKU mapping was added after the original purchase, so the
    // stored metadata.product_id is empty. Reactivation should resolve via
    // Price.metadata and backfill product_id + price_id snapshots so the
    // admin can show the product name and the specific plan.
    const update = jest.fn(async () => undefined);
    const existing = {
      id: 'sub_existing',
      status: 'canceled',
      customer_id: 'cus_1',
      metadata: {},
      payment_details: { app_store: { original_transaction_id: 'orig_1', product_id: 'pro_monthly' } },
      update,
    };
    mockSubscriptionFindOne.mockResolvedValue(existing);
    mockPriceFindOne.mockResolvedValue({ id: 'price_X', product_id: 'prod_X' });
    const future = Date.now() + 30 * 24 * 60 * 60 * 1000;
    const client = makeClient({ expiresDate: future });

    await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client,
      signedTransaction: 'jws-string',
    });

    expect(update).toHaveBeenCalledWith(
      expect.objectContaining({
        status: 'active',
        metadata: expect.objectContaining({ product_id: 'prod_X', price_id: 'price_X' }),
      })
    );
  });

  it('creates a SubscriptionItem pointing at the exact resolved Price (preserves monthly/yearly distinction)', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', update: jest.fn() }]);
    mockPriceFindOne.mockResolvedValue({ id: 'price_X', product_id: 'prod_X' });
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new', livemode: false });

    await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client: makeClient({}),
      signedTransaction: 'jws-string',
    });

    expect(mockSubscriptionItemCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        subscription_id: 'sub_new',
        price_id: 'price_X',
        quantity: 1,
      })
    );
  });

  it('skips SubscriptionItem when no Price has the SKU bound', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', update: jest.fn() }]);
    mockPriceFindOne.mockResolvedValue(null); // admin hasn't wired the binding
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new', livemode: false });

    await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client: makeClient({}),
      signedTransaction: 'jws-string',
    });

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

  it('multi-tenant: Price lookup filters by (sku, bundle_id) so two apps with the same SKU string do not collide', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', update: jest.fn() }]);
    mockPriceFindOne.mockResolvedValue({ id: 'price_X', product_id: 'prod_X' });
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new', livemode: false });

    await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client: makeClient({ productId: 'pro_monthly', bundleId: 'com.appA' }),
      signedTransaction: 'jws-string',
    });

    // The new tenant-scoped lookup must include BOTH the SKU and the
    // bundle_id from the JWS — without bundle_id, App B's Price with the
    // same SKU would silently match.
    expect(mockPriceFindOne).toHaveBeenCalledWith(
      expect.objectContaining({
        where: expect.objectContaining({
          'metadata.app_store_product_id': 'pro_monthly',
          'metadata.bundle_id': 'com.appA',
        }),
      })
    );
    // And the created subscription should persist the bundle_id in
    // payment_details so downstream entitlement queries can disambiguate.
    expect(mockSubscriptionCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        payment_details: expect.objectContaining({
          app_store: expect.objectContaining({ product_id: 'pro_monthly', bundle_id: 'com.appA' }),
        }),
      })
    );
  });

  it('does NOT reactivate a canceled subscription when the new transaction is also expired', async () => {
    const update = jest.fn();
    const existing = { id: 'sub_existing', status: 'canceled', customer_id: 'cus_1', payment_details: { app_store: {} }, update };
    mockSubscriptionFindOne.mockResolvedValue(existing);
    const client = makeClient({ expiresDate: Date.now() - 1000 });

    const result = await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client,
      signedTransaction: 'jws-string',
    });

    expect(result.subscription).toBe(existing);
    expect(update).not.toHaveBeenCalled();
    expect(mockSubscriptionCreate).not.toHaveBeenCalled();
  });

  it('refuses expired purchases', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    const client = makeClient({ expiresDate: Date.now() - 1000 });

    await expect(
      ingestVerifiedAppStorePurchase({
        customerDid: 'did:abc',
        paymentMethod: makeMethod(),
        client,
        signedTransaction: 'jws-string',
      })
    ).rejects.toThrow(/already expired/);
    expect(mockSubscriptionCreate).not.toHaveBeenCalled();
  });

  it('refuses purchases with no expiresDate', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    const client = makeClient({ expiresDate: undefined });

    await expect(
      ingestVerifiedAppStorePurchase({
        customerDid: 'did:abc',
        paymentMethod: makeMethod(),
        client,
        signedTransaction: 'jws-string',
      })
    ).rejects.toThrow(/already expired/);
  });

  it('creates Subscription + Customer mapping on first valid purchase', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    const customerUpdate = jest.fn(async () => undefined);
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', app_store_uuid: undefined, update: customerUpdate }]);
    mockSubscriptionCreate.mockImplementation(async (data) => ({ id: 'sub_new', ...data }));
    const client = makeClient({});

    const result = await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client,
      signedTransaction: 'jws-string',
    });

    expect(result.isFirstSubscribe).toBe(true);
    expect(customerUpdate).toHaveBeenCalledWith({ app_store_uuid: 'uuid-1' });
    expect(mockSubscriptionCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        channel: 'app_store',
        environment: 'sandbox',
        status: 'active',
        customer_id: 'cus_1',
        payment_details: expect.objectContaining({
          app_store: expect.objectContaining({
            original_transaction_id: 'orig_1',
            transaction_id: 'txn_1',
            product_id: 'pro_monthly',
          }),
        }),
      })
    );
    expect(mockCreateEvent).toHaveBeenCalledWith(
      'Subscription',
      'customer.subscription.started',
      expect.objectContaining({ id: 'sub_new' })
    );
  });

  it('skips Customer.update when appAccountToken already matches stored uuid', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    const customerUpdate = jest.fn();
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', app_store_uuid: 'uuid-1', update: customerUpdate }]);
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new' });

    await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client: makeClient({}),
      signedTransaction: 'jws-string',
    });

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

  it('isFirstSubscribe=false when customer already has app_store subs', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockSubscriptionCount.mockResolvedValue(2); // already has 2 prior subs
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', update: jest.fn() }]);
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new' });

    const result = await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client: makeClient({}),
      signedTransaction: 'jws-string',
    });

    expect(result.isFirstSubscribe).toBe(false);
  });

  it('routes to verifyJwsTransaction when signedTransaction is provided', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', update: jest.fn() }]);
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new' });
    const client = makeClient({});

    await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client,
      signedTransaction: 'jws-string',
    });

    expect(client.verifyJwsTransaction).toHaveBeenCalledWith('jws-string');
    expect(client.verifyLegacyReceipt).not.toHaveBeenCalled();
  });

  it('routes to verifyLegacyReceipt when only receipt is provided', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', update: jest.fn() }]);
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new' });
    const client = makeClient({ appAccountToken: undefined });

    await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client,
      receipt: 'base64-receipt-blob',
      expectedProductIds: ['pro_monthly'],
    });

    expect(client.verifyLegacyReceipt).toHaveBeenCalledWith('base64-receipt-blob', {
      expectedProductIds: ['pro_monthly'],
    });
    expect(client.verifyJwsTransaction).not.toHaveBeenCalled();
  });

  it('prefers signedTransaction when both are provided (aistro pattern)', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', update: jest.fn() }]);
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new' });
    const client = makeClient({});

    await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod(),
      client,
      signedTransaction: 'jws-string',
      receipt: 'should-be-ignored',
    });

    expect(client.verifyJwsTransaction).toHaveBeenCalled();
    expect(client.verifyLegacyReceipt).not.toHaveBeenCalled();
  });

  it('throws when neither signedTransaction nor receipt provided', async () => {
    const client = makeClient({});
    await expect(
      ingestVerifiedAppStorePurchase({
        customerDid: 'did:abc',
        paymentMethod: makeMethod(),
        client,
      })
    ).rejects.toThrow(/must provide signedTransaction or receipt/);
  });

  it('maps environment "Production" → "production" in stored Subscription', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockCustomerFindOrCreate.mockResolvedValue([{ id: 'cus_1', update: jest.fn() }]);
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new' });

    await ingestVerifiedAppStorePurchase({
      customerDid: 'did:abc',
      paymentMethod: makeMethod({ livemode: true }),
      client: makeClient({ environment: 'Production' }),
      signedTransaction: 'jws-string',
    });

    expect(mockSubscriptionCreate).toHaveBeenCalledWith(
      expect.objectContaining({ environment: 'production', livemode: true })
    );
  });
});
