// Unit tests for Google Play RTDN dispatch + subscription state machine.
// Models / audit / client are fully mocked — DB is not touched.

import handleGooglePlayEvent, { GooglePlayRtdnPayload } from '../../../src/integrations/google-play/handlers';
import {
  GooglePlayNotificationType,
  handleGooglePlaySubscriptionEvent,
} from '../../../src/integrations/google-play/handlers/subscription';
import type { GooglePlayClient } from '../../../src/integrations/google-play/client';

const mockSubscriptionFindOne: jest.Mock<any, any[]> = jest.fn();
const mockCustomerFindOne: jest.Mock<any, any[]> = jest.fn();
const mockRefundFindOne: jest.Mock<any, any[]> = jest.fn();
// returns a Promise so handler's .catch() works
const mockCreateEvent: jest.Mock<Promise<undefined>, any[]> = jest.fn(async () => undefined);

jest.mock('../../../src/store/models', () => ({
  Subscription: { findOne: (...args: any[]) => mockSubscriptionFindOne(...args) },
  Customer: { findOne: (...args: any[]) => mockCustomerFindOne(...args) },
  Refund: { findOne: (...args: any[]) => mockRefundFindOne(...args) },
}));

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

const makeSubscriptionStub = (overrides: Record<string, any> = {}) => {
  const update = jest.fn(async (patch: any) => Object.assign(stub, patch));
  const stub: any = {
    id: 'sub_test',
    status: 'active',
    current_period_end: 1700000000,
    payment_details: { google_play: { purchase_token: 'tok-1', product_id: 'pro_monthly' } },
    metadata: {},
    update,
    ...overrides,
  };
  return stub;
};

const makeClientMock = (overrides: Partial<GooglePlayClient> = {}): GooglePlayClient => {
  return {
    packageName: 'com.example.app',
    getSubscription: jest.fn(),
    acknowledgeSubscription: jest.fn(),
    ...overrides,
  } as unknown as GooglePlayClient;
};

beforeEach(() => {
  mockSubscriptionFindOne.mockReset();
  mockCustomerFindOne.mockReset();
  mockRefundFindOne.mockReset();
  mockCreateEvent.mockReset();
  mockCreateEvent.mockImplementation(async () => undefined);
});

describe('handleGooglePlayEvent dispatch', () => {
  it('returns silently for testNotification (no work)', async () => {
    const client = makeClientMock();
    const payload: GooglePlayRtdnPayload = {
      version: '1.0',
      packageName: 'com.example.app',
      testNotification: { version: '1.0' },
    };
    await expect(handleGooglePlayEvent(payload, client)).resolves.toBeUndefined();
    expect(mockSubscriptionFindOne).not.toHaveBeenCalled();
    expect(client.acknowledgeSubscription).not.toHaveBeenCalled();
  });

  it('forwards subscriptionNotification to the subscription handler', async () => {
    mockSubscriptionFindOne.mockResolvedValue(makeSubscriptionStub());
    const client = makeClientMock();
    const payload: GooglePlayRtdnPayload = {
      version: '1.0',
      packageName: 'com.example.app',
      subscriptionNotification: {
        version: '1.0',
        notificationType: GooglePlayNotificationType.SUBSCRIPTION_CANCELED,
        purchaseToken: 'tok-1',
        subscriptionId: 'pro_monthly',
      },
    };
    await handleGooglePlayEvent(payload, client);
    expect(mockSubscriptionFindOne).toHaveBeenCalled();
  });

  it('logs but does not throw for oneTimeProductNotification (not handled in A2)', async () => {
    const client = makeClientMock();
    const payload: GooglePlayRtdnPayload = {
      version: '1.0',
      packageName: 'com.example.app',
      oneTimeProductNotification: { version: '1.0', notificationType: 1, purchaseToken: 'tok', sku: 'one_off' },
    };
    await expect(handleGooglePlayEvent(payload, client)).resolves.toBeUndefined();
    expect(mockSubscriptionFindOne).not.toHaveBeenCalled();
  });
});

describe('SUBSCRIPTION_PURCHASED', () => {
  it('acknowledges Google before any further work, even on orphan purchase', async () => {
    const ack = jest.fn();
    const client = makeClientMock({
      acknowledgeSubscription: ack as any,
      getSubscription: (jest.fn().mockResolvedValue({
        obfuscatedExternalAccountId: 'uuid-not-mapped',
      }) as any),
    });

    mockCustomerFindOne.mockResolvedValue(null); // orphan: no customer

    await handleGooglePlaySubscriptionEvent({
      packageName: 'com.example.app',
      client,
      notification: {
        version: '1.0',
        notificationType: GooglePlayNotificationType.SUBSCRIPTION_PURCHASED,
        purchaseToken: 'tok-fresh',
        subscriptionId: 'pro_monthly',
      },
    });

    // ACK was called — this is the 3-day-deadline guarantee
    expect(ack).toHaveBeenCalledWith('pro_monthly', 'tok-fresh');
    // Customer lookup happened but returned null — handler must not crash
    expect(mockCustomerFindOne).toHaveBeenCalledWith({ where: { google_play_uuid: 'uuid-not-mapped' } });
  });

  it('throws when acknowledge fails (so caller can retry)', async () => {
    const ack = jest.fn().mockRejectedValue(new Error('network'));
    const client = makeClientMock({ acknowledgeSubscription: ack as any });
    await expect(
      handleGooglePlaySubscriptionEvent({
        packageName: 'com.example.app',
        client,
        notification: {
          version: '1.0',
          notificationType: GooglePlayNotificationType.SUBSCRIPTION_PURCHASED,
          purchaseToken: 'tok-fresh',
          subscriptionId: 'pro_monthly',
        },
      })
    ).rejects.toThrow(/network/);
  });
});

describe('SUBSCRIPTION_RENEWED', () => {
  it('updates current_period_end + emits customer.subscription.started', async () => {
    const sub = makeSubscriptionStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);
    const client = makeClientMock({
      getSubscription: jest.fn().mockResolvedValue({ expiryTimeMillis: '1800000000000' }) as any,
    });

    await handleGooglePlaySubscriptionEvent({
      packageName: 'com.example.app',
      client,
      notification: {
        version: '1.0',
        notificationType: GooglePlayNotificationType.SUBSCRIPTION_RENEWED,
        purchaseToken: 'tok-1',
        subscriptionId: 'pro_monthly',
      },
    });

    expect(sub.update).toHaveBeenCalledWith(
      expect.objectContaining({ status: 'active', current_period_end: 1800000000 })
    );
    expect(mockCreateEvent).toHaveBeenCalledWith(
      'Subscription',
      'customer.subscription.started',
      sub
    );
  });
});

describe('SUBSCRIPTION_EXPIRED', () => {
  it('marks subscription canceled + emits customer.subscription.deleted', async () => {
    const sub = makeSubscriptionStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);
    const client = makeClientMock();

    await handleGooglePlaySubscriptionEvent({
      packageName: 'com.example.app',
      client,
      notification: {
        version: '1.0',
        notificationType: GooglePlayNotificationType.SUBSCRIPTION_EXPIRED,
        purchaseToken: 'tok-1',
        subscriptionId: 'pro_monthly',
      },
    });

    expect(sub.update).toHaveBeenCalledWith(
      expect.objectContaining({ status: 'canceled' })
    );
    expect(mockCreateEvent).toHaveBeenCalledWith(
      'Subscription',
      'customer.subscription.deleted',
      sub
    );
  });

  it('is idempotent (no update if already canceled)', async () => {
    const sub = makeSubscriptionStub({ status: 'canceled' });
    mockSubscriptionFindOne.mockResolvedValue(sub);
    const client = makeClientMock();

    await handleGooglePlaySubscriptionEvent({
      packageName: 'com.example.app',
      client,
      notification: {
        version: '1.0',
        notificationType: GooglePlayNotificationType.SUBSCRIPTION_EXPIRED,
        purchaseToken: 'tok-1',
        subscriptionId: 'pro_monthly',
      },
    });

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

describe('SUBSCRIPTION_CANCELED', () => {
  it('schedules cancel_at_period_end without immediately ending the subscription', async () => {
    const sub = makeSubscriptionStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);
    const client = makeClientMock();

    await handleGooglePlaySubscriptionEvent({
      packageName: 'com.example.app',
      client,
      notification: {
        version: '1.0',
        notificationType: GooglePlayNotificationType.SUBSCRIPTION_CANCELED,
        purchaseToken: 'tok-1',
        subscriptionId: 'pro_monthly',
      },
    });

    expect(sub.update).toHaveBeenCalledWith({ cancel_at_period_end: true });
    expect(mockCreateEvent).not.toHaveBeenCalledWith(
      'Subscription',
      'customer.subscription.deleted',
      expect.anything()
    );
  });
});

describe('SUBSCRIPTION_ON_HOLD / IN_GRACE_PERIOD', () => {
  it.each([
    GooglePlayNotificationType.SUBSCRIPTION_ON_HOLD,
    GooglePlayNotificationType.SUBSCRIPTION_IN_GRACE_PERIOD,
  ])('marks past_due (type=%i)', async (notificationType) => {
    const sub = makeSubscriptionStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);
    const client = makeClientMock();

    await handleGooglePlaySubscriptionEvent({
      packageName: 'com.example.app',
      client,
      notification: { version: '1.0', notificationType, purchaseToken: 'tok-1', subscriptionId: 'pro_monthly' },
    });

    expect(sub.update).toHaveBeenCalledWith({ status: 'past_due' });
  });
});

describe('SUBSCRIPTION_REVOKED', () => {
  it('marks canceled + emits customer.subscription.deleted (no local Refund row)', async () => {
    const sub = makeSubscriptionStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);

    await handleGooglePlaySubscriptionEvent({
      packageName: 'com.example.app',
      client: makeClientMock(),
      notification: {
        version: '1.0',
        notificationType: GooglePlayNotificationType.SUBSCRIPTION_REVOKED,
        purchaseToken: 'tok-1',
        subscriptionId: 'pro_monthly',
      },
    });

    expect(sub.update).toHaveBeenCalledWith(
      expect.objectContaining({ status: 'canceled' })
    );
    // No local Refund row is created — the store is merchant of record.
    expect(mockRefundFindOne).not.toHaveBeenCalled();
    expect(mockCreateEvent).toHaveBeenCalledWith(
      'Subscription',
      'customer.subscription.deleted',
      sub
    );
  });
});

describe('handler is no-op for unmatched purchase token (notifications other than PURCHASED)', () => {
  it('logs a warning and returns', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    const client = makeClientMock();

    await handleGooglePlaySubscriptionEvent({
      packageName: 'com.example.app',
      client,
      notification: {
        version: '1.0',
        notificationType: GooglePlayNotificationType.SUBSCRIPTION_RENEWED,
        purchaseToken: 'tok-missing',
        subscriptionId: 'pro_monthly',
      },
    });

    expect(client.getSubscription).not.toHaveBeenCalled();
  });
});

describe('SUBSCRIPTION_REVOKED refund marker', () => {
  it('mirrors to canceled and sets metadata.refunded', async () => {
    const sub = makeSubscriptionStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);

    await handleGooglePlaySubscriptionEvent({
      packageName: 'com.example.app',
      client: makeClientMock(),
      notification: {
        version: '1.0',
        notificationType: GooglePlayNotificationType.SUBSCRIPTION_REVOKED,
        purchaseToken: 'tok-1',
        subscriptionId: 'pro_monthly',
      },
    });

    expect(sub.update).toHaveBeenCalledWith(
      expect.objectContaining({
        status: 'canceled',
        metadata: expect.objectContaining({ refunded: true }),
      })
    );
  });
});
