/* eslint-disable require-await, import/first */
// Unit tests for iap-reconcile drift detection.
//
// The DB query path (Subscription.findAll + per-row update) is not exercised
// here — it's a thin loop over the drift functions. Drift detection is the
// only place where state-machine logic lives, so that's what we test.

const mockCreateEvent: jest.Mock<Promise<undefined>, any[]> = jest.fn(async () => undefined);

jest.mock('../../src/store/models', () => ({
  Subscription: {},
  PaymentMethod: {},
}));

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

import {
  applyAppStoreTransactionDrift,
  applyGooglePlayPurchaseDrift,
  pickIapMethodForSub,
} from '../../src/integrations/iap-reconcile';
import type { AppStoreTransactionPayload } from '../../src/integrations/app-store/client';
import type { GooglePlaySubscriptionPurchase } from '../../src/integrations/google-play/client';

type SubFake = {
  id: string;
  status: string;
  current_period_end: number;
  cancel_at_period_end?: boolean;
  payment_details?: any;
  metadata?: any;
  update: jest.Mock;
};

const makeSub = (overrides: Partial<SubFake> = {}): SubFake => {
  const sub: SubFake = {
    id: 'sub_1',
    status: 'active',
    current_period_end: Math.floor(Date.now() / 1000) + 30 * 86400, // 30 days out
    cancel_at_period_end: false,
    payment_details: { app_store: { original_transaction_id: 'orig_1' } },
    metadata: {},
    update: jest.fn(async (patch: any) => Object.assign(sub, patch)) as any,
    ...overrides,
  };
  return sub;
};

const makeAppStoreTxn = (overrides: Partial<AppStoreTransactionPayload> = {}): AppStoreTransactionPayload =>
  ({
    transactionId: 'txn_1',
    originalTransactionId: 'orig_1',
    productId: 'pro_monthly',
    bundleId: 'com.example.app',
    environment: 'Sandbox',
    expiresDate: Date.now() + 30 * 86400 * 1000,
    purchaseDate: Date.now() - 60_000,
    ...overrides,
  }) as AppStoreTransactionPayload;

const makeGooglePurchase = (overrides: Partial<GooglePlaySubscriptionPurchase> = {}): GooglePlaySubscriptionPurchase =>
  ({
    expiryTimeMillis: String(Date.now() + 30 * 86400 * 1000),
    autoRenewing: true,
    ...overrides,
  }) as GooglePlaySubscriptionPurchase;

beforeEach(() => {
  mockCreateEvent.mockClear();
});

describe('pickIapMethodForSub', () => {
  const m = (id: string) => ({ id }) as any;

  it('selects the method bound via default_payment_method_id', () => {
    const methods = [m('pm_a'), m('pm_b')];
    const sub = { id: 's1', default_payment_method_id: 'pm_b' } as any;
    expect(pickIapMethodForSub(methods, sub)?.id).toBe('pm_b');
  });

  it('returns undefined (no arbitrary fallback) when bound method is absent and several exist', () => {
    const methods = [m('pm_a'), m('pm_b')];
    const sub = { id: 's1', default_payment_method_id: 'pm_missing' } as any;
    expect(pickIapMethodForSub(methods, sub)).toBeUndefined();
  });

  it('defaults to the sole method when there is exactly one', () => {
    const methods = [m('pm_only')];
    const sub = { id: 's1', default_payment_method_id: undefined } as any;
    expect(pickIapMethodForSub(methods, sub)?.id).toBe('pm_only');
  });
});

describe('applyAppStoreTransactionDrift', () => {
  it('no-ops when Apple state matches local', async () => {
    const expires = Math.floor(Date.now() / 1000) + 30 * 86400;
    const sub = makeSub({ current_period_end: expires });
    const txn = makeAppStoreTxn({ expiresDate: expires * 1000 });

    const changed = await applyAppStoreTransactionDrift(sub as any, txn);
    expect(changed).toBe(false);
    expect(sub.update).not.toHaveBeenCalled();
  });

  it('updates current_period_end when Apple says we renewed (forward jump)', async () => {
    const oldExpires = Math.floor(Date.now() / 1000) + 86400; // 1 day out
    const newExpiresSec = oldExpires + 30 * 86400; // renewed for 30 more days
    const sub = makeSub({ current_period_end: oldExpires });
    const txn = makeAppStoreTxn({ expiresDate: newExpiresSec * 1000 });

    const changed = await applyAppStoreTransactionDrift(sub as any, txn);
    expect(changed).toBe(true);
    expect(sub.update).toHaveBeenCalledTimes(1);
    const patch = sub.update.mock.calls[0]![0];
    expect(patch.current_period_end).toBe(newExpiresSec);
    expect(patch.status).toBe('active');
    expect(mockCreateEvent).toHaveBeenCalledWith('Subscription', 'customer.subscription.updated', sub);
  });

  it('marks canceled when transaction has revocationDate (refund/revoke)', async () => {
    const sub = makeSub({ status: 'active' });
    const revocationDate = Date.now() - 60_000;
    const txn = makeAppStoreTxn({ revocationDate });

    const changed = await applyAppStoreTransactionDrift(sub as any, txn);
    expect(changed).toBe(true);
    const patch = sub.update.mock.calls[0]![0];
    expect(patch.status).toBe('canceled');
    expect(patch.canceled_at).toBe(Math.floor(revocationDate / 1000));
    expect(patch.cancelation_details.reason).toBe('app_store_revoked');
    expect(mockCreateEvent).toHaveBeenCalledWith('Subscription', 'customer.subscription.deleted', sub);
  });

  it('idempotent: does not re-cancel already-canceled subscription', async () => {
    const sub = makeSub({ status: 'canceled' });
    const txn = makeAppStoreTxn({ revocationDate: Date.now() - 60_000 });

    const changed = await applyAppStoreTransactionDrift(sub as any, txn);
    expect(changed).toBe(false);
    expect(sub.update).not.toHaveBeenCalled();
  });

  it('marks canceled when Apple says expired but local still active (webhook miss on EXPIRED)', async () => {
    const expiredSec = Math.floor(Date.now() / 1000) - 3600; // 1h in the past
    const sub = makeSub({ status: 'active', current_period_end: expiredSec });
    const txn = makeAppStoreTxn({ expiresDate: expiredSec * 1000 });

    const changed = await applyAppStoreTransactionDrift(sub as any, txn);
    expect(changed).toBe(true);
    const patch = sub.update.mock.calls[0]![0];
    expect(patch.status).toBe('canceled');
    expect(patch.cancelation_details.reason).toBe('app_store_expired');
  });
});

describe('applyGooglePlayPurchaseDrift', () => {
  it('no-ops when Google state matches local', async () => {
    const expires = Math.floor(Date.now() / 1000) + 30 * 86400;
    const sub = makeSub({
      current_period_end: expires,
      payment_details: { google_play: { purchase_token: 'tok_1' } },
    });
    const purchase = makeGooglePurchase({ expiryTimeMillis: String(expires * 1000) });

    const changed = await applyGooglePlayPurchaseDrift(sub as any, purchase);
    expect(changed).toBe(false);
    expect(sub.update).not.toHaveBeenCalled();
  });

  it('updates current_period_end when Google says we renewed', async () => {
    const oldExpires = Math.floor(Date.now() / 1000) + 86400;
    const newExpiresSec = oldExpires + 30 * 86400;
    const sub = makeSub({
      current_period_end: oldExpires,
      payment_details: { google_play: { purchase_token: 'tok_1' } },
    });
    const purchase = makeGooglePurchase({ expiryTimeMillis: String(newExpiresSec * 1000) });

    const changed = await applyGooglePlayPurchaseDrift(sub as any, purchase);
    expect(changed).toBe(true);
    const patch = sub.update.mock.calls[0]![0];
    expect(patch.current_period_end).toBe(newExpiresSec);
    expect(mockCreateEvent).toHaveBeenCalledWith('Subscription', 'customer.subscription.updated', sub);
  });

  it('sets cancel_at_period_end when user disabled auto-renew (cancelReason set, autoRenewing=false)', async () => {
    const sub = makeSub({
      cancel_at_period_end: false,
      payment_details: { google_play: { purchase_token: 'tok_1' } },
    });
    const purchase = makeGooglePurchase({
      expiryTimeMillis: String(sub.current_period_end * 1000), // not a renewal drift
      cancelReason: 0,
      autoRenewing: false,
    });

    const changed = await applyGooglePlayPurchaseDrift(sub as any, purchase);
    expect(changed).toBe(true);
    const patch = sub.update.mock.calls[0]![0];
    expect(patch.cancel_at_period_end).toBe(true);
    expect(patch.cancelation_details.reason).toBe('google_play_auto_renew_off');
  });

  it('idempotent: does not re-flag cancel_at_period_end when already set', async () => {
    const sub = makeSub({
      cancel_at_period_end: true,
      payment_details: { google_play: { purchase_token: 'tok_1' } },
    });
    const purchase = makeGooglePurchase({
      expiryTimeMillis: String(sub.current_period_end * 1000),
      cancelReason: 0,
      autoRenewing: false,
    });

    const changed = await applyGooglePlayPurchaseDrift(sub as any, purchase);
    expect(changed).toBe(false);
    expect(sub.update).not.toHaveBeenCalled();
  });

  it('marks canceled when Google says expired but local still active', async () => {
    const expiredSec = Math.floor(Date.now() / 1000) - 3600;
    const sub = makeSub({
      status: 'active',
      current_period_end: expiredSec,
      payment_details: { google_play: { purchase_token: 'tok_1' } },
    });
    const purchase = makeGooglePurchase({ expiryTimeMillis: String(expiredSec * 1000) });

    const changed = await applyGooglePlayPurchaseDrift(sub as any, purchase);
    expect(changed).toBe(true);
    const patch = sub.update.mock.calls[0]![0];
    expect(patch.status).toBe('canceled');
    expect(patch.cancelation_details.reason).toBe('google_play_expired');
  });
});
