/* eslint-disable require-await, import/first */
// Unit tests for App Store Server Notifications V2 dispatch + state machine.
// Models / audit / client are fully mocked — DB is not touched.

process.env.APP_STORE_SKIP_SIGNATURE_VERIFY = 'true';

import handleAppStoreNotification from '../../../src/integrations/app-store/handlers';
import { handleAppStoreSubscriptionEvent } from '../../../src/integrations/app-store/handlers/subscription';
import type {
  AppStoreClient,
  AppStoreNotificationPayload,
  AppStoreNotificationType,
  AppStoreTransactionPayload,
} from '../../../src/integrations/app-store/client';

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

jest.mock('../../../src/store/models', () => ({
  Subscription: {
    findOne: (...args: any[]) => mockSubscriptionFindOne(...args),
    create: (...args: any[]) => mockSubscriptionCreate(...args),
  },
  Customer: {
    findOne: (...args: any[]) => mockCustomerFindOne(...args),
  },
  Price: { findOne: (...args: any[]) => mockPriceFindOne(...args) },
  PaymentMethod: {
    findOne: (...args: any[]) => mockPaymentMethodFindOne(...args),
  },
}));

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

const baseTransaction: AppStoreTransactionPayload = {
  transactionId: 'txn_1',
  originalTransactionId: 'orig_1',
  productId: 'pro_monthly',
  bundleId: 'com.example.app',
  environment: 'Sandbox',
  expiresDate: Date.now() + 30 * 24 * 60 * 60 * 1000,
  purchaseDate: Date.now() - 60_000,
  appAccountToken: 'uuid-1',
};

const makeNotification = (
  type: AppStoreNotificationType,
  overrides: Partial<AppStoreNotificationPayload> = {}
): AppStoreNotificationPayload => ({
  notificationType: type,
  notificationUUID: 'uuid-notif',
  version: '2.0',
  signedDate: Date.now(),
  data: {
    bundleId: 'com.example.app',
    environment: 'Sandbox',
    signedTransactionInfo: 'inner-jws-placeholder',
  },
  ...overrides,
});

const makeSubStub = (overrides: Record<string, any> = {}) => {
  const update = jest.fn(async (patch: any) => Object.assign(stub, patch));
  const stub: any = {
    id: 'sub_test',
    status: 'active',
    cancel_at_period_end: false,
    current_period_end: 1700000000,
    payment_details: { app_store: { original_transaction_id: 'orig_1', product_id: 'pro_monthly' } },
    metadata: {},
    update,
    ...overrides,
  };
  return stub;
};

const makeClient = (overrides: Partial<AppStoreClient> = {}): AppStoreClient =>
  ({
    bundleId: 'com.example.app',
    environment: 'sandbox',
    verifyJwsTransaction: jest.fn(async () => baseTransaction),
    ...overrides,
  }) as unknown as AppStoreClient;

beforeEach(() => {
  mockSubscriptionFindOne.mockReset();
  mockSubscriptionCreate.mockReset();
  mockCustomerFindOne.mockReset();
  mockPriceFindOne.mockReset().mockResolvedValue(null);
  mockPaymentMethodFindOne.mockReset();
  mockCreateEvent.mockReset().mockImplementation(async () => undefined);
});

describe('handleAppStoreNotification dispatch', () => {
  it('returns silently for TEST notification', async () => {
    const client = makeClient();
    await handleAppStoreNotification(makeNotification('TEST'), client);
    expect(client.verifyJwsTransaction).not.toHaveBeenCalled();
    expect(mockSubscriptionFindOne).not.toHaveBeenCalled();
  });

  it('returns silently when signedTransactionInfo is absent', async () => {
    const client = makeClient();
    const notif = makeNotification('DID_RENEW', {
      data: { bundleId: 'com.example.app', environment: 'Sandbox' },
    });
    await handleAppStoreNotification(notif, client);
    expect(client.verifyJwsTransaction).not.toHaveBeenCalled();
  });

  it('decodes inner JWS and forwards to state machine for actionable notifications', async () => {
    mockSubscriptionFindOne.mockResolvedValue(makeSubStub());
    const client = makeClient();
    await handleAppStoreNotification(makeNotification('DID_RENEW'), client);
    expect(client.verifyJwsTransaction).toHaveBeenCalledWith('inner-jws-placeholder');
    expect(mockSubscriptionFindOne).toHaveBeenCalled();
  });
});

describe('SUBSCRIBED', () => {
  it('is idempotent — does nothing when local Subscription already exists', async () => {
    const existing = makeSubStub({ id: 'sub_existing' });
    mockSubscriptionFindOne.mockResolvedValue(existing);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('SUBSCRIBED'),
      transaction: baseTransaction,
    });

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

  it('logs orphan warning when transaction has no appAccountToken', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('SUBSCRIBED'),
      transaction: { ...baseTransaction, appAccountToken: undefined },
    });

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

  it('logs orphan warning when no Customer matches appAccountToken', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockCustomerFindOne.mockResolvedValue(null);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('SUBSCRIBED'),
      transaction: baseTransaction,
    });

    expect(mockCustomerFindOne).toHaveBeenCalledWith({ where: { app_store_uuid: 'uuid-1' } });
    expect(mockSubscriptionCreate).not.toHaveBeenCalled();
  });

  it('creates Subscription when Customer + PaymentMethod both found', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);
    mockCustomerFindOne.mockResolvedValue({ id: 'cus_1', livemode: false });
    mockPaymentMethodFindOne.mockResolvedValue({ id: 'pm_1', default_currency_id: 'cur_1' });
    mockSubscriptionCreate.mockResolvedValue({ id: 'sub_new' });

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('SUBSCRIBED', { subtype: 'INITIAL_BUY' }),
      transaction: baseTransaction,
    });

    expect(mockSubscriptionCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        channel: 'app_store',
        environment: 'sandbox',
        status: 'active',
        customer_id: 'cus_1',
        default_payment_method_id: 'pm_1',
      })
    );
    expect(mockCreateEvent).toHaveBeenCalledWith(
      'Subscription',
      'customer.subscription.started',
      expect.objectContaining({ id: 'sub_new' })
    );
  });
});

describe('DID_RENEW', () => {
  it('updates current_period_end + emits customer.subscription.started', async () => {
    const sub = makeSubStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);
    const newExpiryMs = Date.now() + 60 * 24 * 60 * 60 * 1000;

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('DID_RENEW'),
      transaction: { ...baseTransaction, expiresDate: newExpiryMs },
    });

    expect(sub.update).toHaveBeenCalledWith(
      expect.objectContaining({
        status: 'active',
        current_period_end: Math.floor(newExpiryMs / 1000),
      })
    );
    expect(mockCreateEvent).toHaveBeenCalledWith('Subscription', 'customer.subscription.started', sub);
  });
});

describe('EXPIRED / GRACE_PERIOD_EXPIRED', () => {
  it.each(['EXPIRED', 'GRACE_PERIOD_EXPIRED'] as AppStoreNotificationType[])(
    'marks canceled + emits deleted (type=%s)',
    async (type) => {
      const sub = makeSubStub();
      mockSubscriptionFindOne.mockResolvedValue(sub);

      await handleAppStoreSubscriptionEvent({
        notification: makeNotification(type),
        transaction: baseTransaction,
      });

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

  it('is idempotent when already canceled', async () => {
    const sub = makeSubStub({ status: 'canceled' });
    mockSubscriptionFindOne.mockResolvedValue(sub);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('EXPIRED'),
      transaction: baseTransaction,
    });

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

describe('DID_FAIL_TO_RENEW', () => {
  it('marks past_due', async () => {
    const sub = makeSubStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('DID_FAIL_TO_RENEW'),
      transaction: baseTransaction,
    });

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

describe('DID_CHANGE_RENEWAL_STATUS', () => {
  it('AUTO_RENEW_DISABLED schedules cancel_at_period_end', async () => {
    const sub = makeSubStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('DID_CHANGE_RENEWAL_STATUS', { subtype: 'AUTO_RENEW_DISABLED' }),
      transaction: baseTransaction,
    });

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

  it('AUTO_RENEW_ENABLED un-schedules cancel', async () => {
    const sub = makeSubStub({ cancel_at_period_end: true });
    mockSubscriptionFindOne.mockResolvedValue(sub);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('DID_CHANGE_RENEWAL_STATUS', { subtype: 'AUTO_RENEW_ENABLED' }),
      transaction: baseTransaction,
    });

    expect(sub.update).toHaveBeenCalledWith({ cancel_at_period_end: false });
  });
});

describe('REVOKE / REFUND', () => {
  it.each(['REVOKE', 'REFUND'] as AppStoreNotificationType[])(
    'marks canceled + records cancelation_details + emits deleted (type=%s)',
    async (type) => {
      const sub = makeSubStub();
      mockSubscriptionFindOne.mockResolvedValue(sub);

      await handleAppStoreSubscriptionEvent({
        notification: makeNotification(type),
        transaction: baseTransaction,
      });

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

describe('Informational types', () => {
  it.each([
    'PRICE_INCREASE',
    'DID_CHANGE_RENEWAL_PREF',
    'RENEWAL_EXTENDED',
    'REFUND_DECLINED',
  ] as AppStoreNotificationType[])('records metadata without state change (type=%s)', async (type) => {
    const sub = makeSubStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification(type),
      transaction: baseTransaction,
    });

    expect(sub.update).toHaveBeenCalledWith(
      expect.objectContaining({
        metadata: expect.objectContaining({ app_store_last_notification_type: type }),
      })
    );
    expect(mockCreateEvent).not.toHaveBeenCalled();
  });
});

describe('Subscription not found', () => {
  it('logs warn and no-ops for non-SUBSCRIBED notifications when local sub is missing', async () => {
    mockSubscriptionFindOne.mockResolvedValue(null);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('DID_RENEW'),
      transaction: baseTransaction,
    });

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

describe('REFUND / REVOKE marker', () => {
  it('REFUND mirrors to canceled and sets metadata.refunded', async () => {
    const sub = makeSubStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('REFUND'),
      transaction: baseTransaction,
    });

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

  it('REVOKE mirrors to canceled but does NOT set metadata.refunded', async () => {
    const sub = makeSubStub();
    mockSubscriptionFindOne.mockResolvedValue(sub);

    await handleAppStoreSubscriptionEvent({
      notification: makeNotification('REVOKE'),
      transaction: baseTransaction,
    });

    const patch = sub.update.mock.calls[0][0];
    expect(patch.status).toBe('canceled');
    expect(patch.metadata.refunded).toBeUndefined();
  });
});
