// Unit tests for AppStoreClient.
// All synthetic JWS fixtures here can't pass real Apple signature verification,
// so we set APP_STORE_SKIP_SIGNATURE_VERIFY=true to exercise the decode/validate
// surface. A dedicated test below confirms real-mode rejects synthetic JWSes.

/* eslint-disable import/first */
process.env.APP_STORE_SKIP_SIGNATURE_VERIFY = 'true';

import { AppStoreClient, AppStoreSettings } from '../../../src/integrations/app-store/client';

const mockConfigApple = jest.fn();
const mockValidateAppleReceipt = jest.fn();
jest.mock('node-apple-receipt-verify', () => ({
  config: (...args: any[]) => mockConfigApple(...args),
  validate: (...args: any[]) => mockValidateAppleReceipt(...args),
}));

beforeEach(() => {
  mockConfigApple.mockReset();
  mockValidateAppleReceipt.mockReset();
});

const baseSettings: AppStoreSettings = {
  bundle_id: 'com.example.app',
  environment: 'sandbox',
};

const makeJws = (payload: object, header: object = { alg: 'ES256' }): string => {
  const h = Buffer.from(JSON.stringify(header)).toString('base64url');
  const p = Buffer.from(JSON.stringify(payload)).toString('base64url');
  return `${h}.${p}.signature-placeholder`;
};

describe('AppStoreClient.fromSettings', () => {
  it('rejects missing bundle_id', () => {
    expect(() => AppStoreClient.fromSettings({ ...baseSettings, bundle_id: '' })).toThrow(/bundle_id/);
  });

  it('rejects bad environment', () => {
    expect(() => AppStoreClient.fromSettings({ ...baseSettings, environment: 'staging' as any })).toThrow(
      /environment/
    );
  });

  it('accepts minimal valid settings (no Server API creds)', () => {
    const c = AppStoreClient.fromSettings(baseSettings);
    expect(c.bundleId).toBe('com.example.app');
    expect(c.environment).toBe('sandbox');
  });
});

describe('AppStoreClient.verifyJwsTransaction (mock-phase decode)', () => {
  let client: AppStoreClient;
  beforeEach(() => {
    client = AppStoreClient.fromSettings(baseSettings);
  });

  it('decodes a well-formed JWS payload', async () => {
    const jws = makeJws({
      transactionId: 'txn_1',
      originalTransactionId: 'orig_1',
      productId: 'pro_monthly',
      expiresDate: 1800000000000,
      appAccountToken: 'uuid-from-client',
      environment: 'Sandbox',
      bundleId: 'com.example.app',
    });
    const decoded = await client.verifyJwsTransaction(jws);
    expect(decoded.transactionId).toBe('txn_1');
    expect(decoded.originalTransactionId).toBe('orig_1');
    expect(decoded.productId).toBe('pro_monthly');
    expect(decoded.appAccountToken).toBe('uuid-from-client');
    expect(decoded.environment).toBe('Sandbox');
  });

  it('rejects malformed JWS (not 3 segments)', async () => {
    await expect(client.verifyJwsTransaction('not-a-jws')).rejects.toThrow(/format invalid/);
    await expect(client.verifyJwsTransaction('a.b')).rejects.toThrow(/format invalid/);
  });

  it('rejects JWS whose payload is not valid JSON', async () => {
    const h = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString('base64url');
    const broken = Buffer.from('this is not json').toString('base64url');
    await expect(client.verifyJwsTransaction(`${h}.${broken}.sig`)).rejects.toThrow(/not valid JSON/);
  });

  it('rejects JWS whose bundleId disagrees with configured bundle_id', async () => {
    const jws = makeJws({
      transactionId: 'txn_1',
      originalTransactionId: 'orig_1',
      productId: 'pro_monthly',
      environment: 'Sandbox',
      bundleId: 'com.evil.app',
    });
    await expect(client.verifyJwsTransaction(jws)).rejects.toThrow(/bundleId mismatch/);
  });

  it('passes when JWS payload omits bundleId (older StoreKit 2 payloads)', async () => {
    const jws = makeJws({
      transactionId: 'txn_1',
      originalTransactionId: 'orig_1',
      productId: 'pro_monthly',
      environment: 'Sandbox',
    });
    const decoded = await client.verifyJwsTransaction(jws);
    expect(decoded.transactionId).toBe('txn_1');
  });
});

describe('AppStoreClient write methods', () => {
  it('refundSubscription is gated unless APP_STORE_WRITE_ENABLED', async () => {
    const c = AppStoreClient.fromSettings(baseSettings);
    await expect(c.refundSubscription('orig_1')).rejects.toThrow(/APP_STORE_WRITE_ENABLED/);
  });

  it('cancelSubscription is gated unless APP_STORE_WRITE_ENABLED', async () => {
    const c = AppStoreClient.fromSettings(baseSettings);
    await expect(c.cancelSubscription('orig_1')).rejects.toThrow(/APP_STORE_WRITE_ENABLED/);
  });

  it('getSubscriptionStatus errors clearly when Server API creds are not configured', async () => {
    const c = AppStoreClient.fromSettings(baseSettings);
    await expect(c.getSubscriptionStatus('orig_1')).rejects.toThrow(/issuer_id\/key_id\/private_key_pem/);
  });
});

// `getSubscriptionStatus` against a mocked Apple SDK — we don't hit the real API.
const mockGetAllSubscriptionStatuses = jest.fn();
jest.mock('../../../src/integrations/app-store/signed-data-verifier', () => {
  const actual = jest.requireActual('../../../src/integrations/app-store/signed-data-verifier');
  return {
    ...actual,
    getAllSubscriptionStatuses: (...args: any[]) => mockGetAllSubscriptionStatuses(...args),
  };
});

describe('AppStoreClient.getSubscriptionStatus (Server API)', () => {
  beforeEach(() => {
    mockGetAllSubscriptionStatuses.mockReset();
  });

  const credSettings: AppStoreSettings = {
    bundle_id: 'com.example.app',
    environment: 'sandbox',
    issuer_id: 'iss_1',
    key_id: 'KEY_ID_1',
    private_key_pem: '-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----',
  };

  it('returns decoded transaction payload when Apple returns matching txn', async () => {
    const inner = (() => {
      const h = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString('base64url');
      const p = Buffer.from(
        JSON.stringify({
          transactionId: 'txn_latest',
          originalTransactionId: 'orig_X',
          productId: 'sub_06',
          bundleId: 'com.example.app',
          environment: 'Sandbox',
          expiresDate: 1900000000000,
        })
      ).toString('base64url');
      return `${h}.${p}.sig`;
    })();
    mockGetAllSubscriptionStatuses.mockResolvedValue({
      data: [{ lastTransactions: [{ originalTransactionId: 'orig_X', signedTransactionInfo: inner }] }],
    });
    const c = AppStoreClient.fromSettings(credSettings);
    const result = await c.getSubscriptionStatus('orig_X');
    expect(result?.transactionId).toBe('txn_latest');
    expect(mockGetAllSubscriptionStatuses).toHaveBeenCalledWith(
      'orig_X',
      expect.objectContaining({ bundleId: 'com.example.app' })
    );
  });

  it('returns null when Apple returns no matching transaction', async () => {
    mockGetAllSubscriptionStatuses.mockResolvedValue({ data: [] });
    const c = AppStoreClient.fromSettings(credSettings);
    expect(await c.getSubscriptionStatus('orig_unknown')).toBeNull();
  });
});

describe('AppStoreClient.verifyNotificationPayload (mock-phase decode)', () => {
  let client: AppStoreClient;
  beforeEach(() => {
    client = AppStoreClient.fromSettings(baseSettings);
  });

  it('decodes a well-formed signedPayload', async () => {
    const jws = makeJws({
      notificationType: 'DID_RENEW',
      subtype: 'BILLING_RECOVERY',
      notificationUUID: 'uuid-notif-1',
      version: '2.0',
      signedDate: 1700000000000,
      data: {
        bundleId: 'com.example.app',
        environment: 'Sandbox',
        signedTransactionInfo: 'inner-jws',
        status: 1,
      },
    });
    const decoded = await client.verifyNotificationPayload(jws);
    expect(decoded.notificationType).toBe('DID_RENEW');
    expect(decoded.subtype).toBe('BILLING_RECOVERY');
    expect(decoded.data.environment).toBe('Sandbox');
    expect(decoded.data.signedTransactionInfo).toBe('inner-jws');
  });

  it('rejects malformed JWS (not 3 segments)', async () => {
    await expect(client.verifyNotificationPayload('a.b')).rejects.toThrow(/JWS format invalid/);
  });

  it('rejects when notification bundleId disagrees with configured one', async () => {
    const jws = makeJws({
      notificationType: 'DID_RENEW',
      notificationUUID: 'uuid-1',
      version: '2.0',
      signedDate: 1700000000000,
      data: { bundleId: 'com.evil.app', environment: 'Sandbox' },
    });
    await expect(client.verifyNotificationPayload(jws)).rejects.toThrow(/bundleId mismatch/);
  });
});

describe('AppStoreClient.verifyLegacyReceipt', () => {
  const withSecret: AppStoreSettings = { ...baseSettings, shared_secret: 'sk_test_secret' };

  it('refuses when shared_secret is not configured', async () => {
    const c = AppStoreClient.fromSettings(baseSettings);
    await expect(c.verifyLegacyReceipt('base64-receipt')).rejects.toThrow(/shared_secret is required/);
  });

  it('configures node-apple-receipt-verify with provided secret + both envs', async () => {
    mockValidateAppleReceipt.mockResolvedValue([
      {
        bundleId: 'com.example.app',
        productId: 'sub_06',
        transactionId: 'txn_1',
        originalTransactionId: 'orig_1',
        purchaseDate: 1700000000000,
        expirationDate: 1800000000000,
      },
    ]);
    const c = AppStoreClient.fromSettings(withSecret);
    await c.verifyLegacyReceipt('base64-receipt');
    expect(mockConfigApple).toHaveBeenCalledWith(
      expect.objectContaining({
        secret: 'sk_test_secret',
        environment: ['production', 'sandbox'],
      })
    );
  });

  it('normalizes one-item receipt to AppStoreTransactionPayload', async () => {
    mockValidateAppleReceipt.mockResolvedValue([
      {
        bundleId: 'com.example.app',
        productId: 'sub_06',
        transactionId: 'txn_1',
        originalTransactionId: 'orig_1',
        purchaseDate: 1700000000000,
        expirationDate: 1800000000000,
      },
    ]);
    const c = AppStoreClient.fromSettings(withSecret);
    const result = await c.verifyLegacyReceipt('base64-receipt');
    expect(result).toEqual({
      transactionId: 'txn_1',
      originalTransactionId: 'orig_1',
      productId: 'sub_06',
      purchaseDate: 1700000000000,
      expiresDate: 1800000000000,
      appAccountToken: undefined,
      environment: 'Sandbox',
      bundleId: 'com.example.app',
      webOrderLineItemId: undefined,
    });
  });

  it('picks the item with the latest expirationDate when receipt contains multiple', async () => {
    mockValidateAppleReceipt.mockResolvedValue([
      { productId: 'sub_06', transactionId: 'old', expirationDate: 1500000000000 },
      { productId: 'sub_06', transactionId: 'new', expirationDate: 1800000000000 },
      { productId: 'sub_06', transactionId: 'mid', expirationDate: 1600000000000 },
    ]);
    const c = AppStoreClient.fromSettings(withSecret);
    const result = await c.verifyLegacyReceipt('base64-receipt');
    expect(result.transactionId).toBe('new');
  });

  it('filters by expectedProductIds when provided', async () => {
    mockValidateAppleReceipt.mockResolvedValue([
      { productId: 'other_sku', transactionId: 'wrong', expirationDate: 1900000000000 },
      { productId: 'sub_06', transactionId: 'right', expirationDate: 1800000000000 },
    ]);
    const c = AppStoreClient.fromSettings(withSecret);
    const result = await c.verifyLegacyReceipt('base64-receipt', { expectedProductIds: ['sub_06'] });
    expect(result.transactionId).toBe('right');
  });

  it('throws when no item matches expectedProductIds', async () => {
    mockValidateAppleReceipt.mockResolvedValue([
      { productId: 'unknown_sku', transactionId: 'x', expirationDate: 1800000000000 },
    ]);
    const c = AppStoreClient.fromSettings(withSecret);
    await expect(c.verifyLegacyReceipt('base64-receipt', { expectedProductIds: ['sub_06'] })).rejects.toThrow(
      /no matching purchases/
    );
  });

  it('rejects when receipt bundleId disagrees with configured bundle_id', async () => {
    mockValidateAppleReceipt.mockResolvedValue([
      {
        bundleId: 'com.evil.app',
        productId: 'sub_06',
        transactionId: 'txn_1',
        originalTransactionId: 'orig_1',
        expirationDate: 1800000000000,
      },
    ]);
    const c = AppStoreClient.fromSettings(withSecret);
    await expect(c.verifyLegacyReceipt('base64-receipt')).rejects.toThrow(/bundleId mismatch/);
  });

  it('falls back to transactionId when originalTransactionId is absent', async () => {
    mockValidateAppleReceipt.mockResolvedValue([
      { productId: 'sub_06', transactionId: 'txn_only', expirationDate: 1800000000000 },
    ]);
    const c = AppStoreClient.fromSettings(withSecret);
    const result = await c.verifyLegacyReceipt('base64-receipt');
    expect(result.originalTransactionId).toBe('txn_only');
  });
});
