import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { RazorpayConnection, RazorpayEnvironment } from '@insforge/shared-schemas';
import type { RazorpayProvider } from '../../src/providers/payments/razorpay.provider';

const { mockConfigService, mockPool, mockWithPaymentSessionAdvisoryLock } = vi.hoisted(() => ({
  mockConfigService: {
    listRazorpayEnvironments: vi.fn(),
    createRazorpayProvider: vi.fn(),
    recordConnectionStatus: vi.fn(),
    writeSnapshot: vi.fn(),
    writeFailedSnapshot: vi.fn(),
    getConnection: vi.fn(),
  },
  mockPool: {
    connect: vi.fn(),
  },
  mockWithPaymentSessionAdvisoryLock: vi.fn(),
}));

vi.mock('../../src/infra/database/database.manager', () => ({
  DatabaseManager: {
    getInstance: () => ({
      getPool: () => mockPool,
    }),
  },
}));

vi.mock('../../src/services/payments/payments-advisory-lock', () => ({
  withPaymentSessionAdvisoryLock: mockWithPaymentSessionAdvisoryLock,
}));

vi.mock('../../src/services/payments/razorpay/config.service', () => ({
  RazorpayConfigService: {
    getInstance: () => mockConfigService,
  },
}));

import { RazorpaySyncService } from '../../src/services/payments/razorpay/sync.service';

function makeConnection(
  environment: RazorpayEnvironment,
  status: RazorpayConnection['status'],
  lastSyncError: string | null
): RazorpayConnection {
  return {
    environment,
    status,
    accountId: null,
    merchantName: null,
    accountLivemode: null,
    webhookEndpointId: null,
    webhookEndpointUrl: null,
    webhookConfiguredAt: null,
    maskedKey: null,
    lastSyncedAt: null,
    lastSyncStatus: 'failed',
    lastSyncError,
    lastSyncCounts: {},
  };
}

describe('RazorpaySyncService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    const mockClient = {
      query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
      release: vi.fn(),
    };
    mockWithPaymentSessionAdvisoryLock.mockImplementation(
      async (_pool: unknown, _lockName: string, task: () => Promise<unknown>) => task()
    );
    mockPool.connect.mockResolvedValue(mockClient);
    mockConfigService.listRazorpayEnvironments.mockReturnValue(['test', 'live']);
    mockConfigService.createRazorpayProvider.mockRejectedValue(new Error('missing keys'));
    mockConfigService.recordConnectionStatus.mockImplementation(
      async (
        environment: RazorpayEnvironment,
        status: RazorpayConnection['status'],
        error: string
      ) => makeConnection(environment, status, error)
    );
    mockConfigService.writeSnapshot.mockResolvedValue(undefined);
    mockConfigService.writeFailedSnapshot.mockResolvedValue(undefined);
    mockConfigService.getConnection.mockImplementation(async (environment: RazorpayEnvironment) =>
      makeConnection(environment, 'connected', 'customers: customer API unavailable')
    );
  });

  it('returns failed unconfigured results for missing keys while using environment locks', async () => {
    const service = RazorpaySyncService.getInstance();

    const result = await service.syncAll('test');

    expect(mockWithPaymentSessionAdvisoryLock).toHaveBeenCalledWith(
      mockPool,
      'payments_razorpay_environment_test',
      expect.any(Function)
    );
    expect(mockConfigService.recordConnectionStatus).toHaveBeenCalledWith(
      'test',
      'unconfigured',
      'missing keys'
    );
    expect(result).toEqual({
      results: [
        {
          environment: 'test',
          status: 'failed',
          connection: makeConnection('test', 'unconfigured', 'missing keys'),
          syncCounts: {
            plans: 0,
            items: 0,
            customers: 0,
            subscriptions: 0,
            invoices: 0,
            payments: 0,
          },
          error: 'missing keys',
        },
      ],
    });
  });

  it('syncs all supported Razorpay environments through the same result envelope', async () => {
    const service = RazorpaySyncService.getInstance();

    const result = await service.syncAll('all');

    expect(result.results.map((item) => item.environment)).toEqual(['test', 'live']);
    expect(mockWithPaymentSessionAdvisoryLock).toHaveBeenCalledWith(
      mockPool,
      'payments_razorpay_environment_test',
      expect.any(Function)
    );
    expect(mockWithPaymentSessionAdvisoryLock).toHaveBeenCalledWith(
      mockPool,
      'payments_razorpay_environment_live',
      expect.any(Function)
    );
  });

  it('marks the environment failed when a non-catalog sync stage fails', async () => {
    const provider = {
      syncCatalog: vi.fn().mockResolvedValue({
        account: {
          id: 'acc_123',
          merchantName: 'Example Merchant',
          livemode: false,
        },
        plans: [],
        items: [],
      }),
      listCustomers: vi.fn().mockRejectedValue(new Error('customer API unavailable')),
      listSubscriptions: vi.fn().mockResolvedValue([]),
      listInvoices: vi.fn().mockResolvedValue([]),
      listPayments: vi.fn().mockResolvedValue([]),
    };
    mockConfigService.createRazorpayProvider.mockResolvedValue(provider);

    const result = await RazorpaySyncService.getInstance().syncAll('test');

    expect(mockConfigService.writeSnapshot).not.toHaveBeenCalled();
    expect(mockConfigService.writeFailedSnapshot).toHaveBeenCalledWith(
      'test',
      'acc_123',
      'Example Merchant',
      false,
      {
        plans: 0,
        items: 0,
        customers: 0,
        subscriptions: 0,
        invoices: 0,
        payments: 0,
      },
      'customers: customer API unavailable',
      expect.any(Date)
    );
    expect(result.results[0]).toEqual(
      expect.objectContaining({
        environment: 'test',
        status: 'failed',
        syncCounts: {
          plans: 0,
          items: 0,
          customers: 0,
          subscriptions: 0,
          invoices: 0,
          payments: 0,
        },
        error: 'customers: customer API unavailable',
      })
    );
  });

  it('does not count fetched Razorpay records when the stage fails to persist them', async () => {
    const mockClient = {
      query: vi.fn().mockImplementation(async (sql: string) => {
        if (/INSERT INTO payments\.customers/i.test(sql)) {
          throw new Error('customer upsert failed');
        }

        return { rows: [], rowCount: 0 };
      }),
      release: vi.fn(),
    };
    mockPool.connect.mockResolvedValue(mockClient);
    const provider = {
      syncCatalog: vi.fn().mockResolvedValue({
        account: {
          id: 'acc_123',
          merchantName: 'Example Merchant',
          livemode: false,
        },
        plans: [],
        items: [],
      }),
      listCustomers: vi.fn().mockResolvedValue([
        {
          id: 'cust_123',
          entity: 'customer',
          name: 'Buyer',
          email: 'buyer@example.com',
          contact: null,
          notes: {},
          created_at: 1780617600,
        },
      ]),
      listSubscriptions: vi.fn().mockResolvedValue([]),
      listInvoices: vi.fn().mockResolvedValue([]),
      listPayments: vi.fn().mockResolvedValue([]),
    };
    mockConfigService.createRazorpayProvider.mockResolvedValue(provider);

    const result = await RazorpaySyncService.getInstance().syncAll('test');

    expect(mockConfigService.writeFailedSnapshot).toHaveBeenCalledWith(
      'test',
      'acc_123',
      'Example Merchant',
      false,
      {
        plans: 0,
        items: 0,
        customers: 0,
        subscriptions: 0,
        invoices: 0,
        payments: 0,
      },
      'customers: customer upsert failed',
      expect.any(Date)
    );
    expect(result.results[0]).toEqual(
      expect.objectContaining({
        status: 'failed',
        syncCounts: {
          plans: 0,
          items: 0,
          customers: 0,
          subscriptions: 0,
          invoices: 0,
          payments: 0,
        },
        error: 'customers: customer upsert failed',
      })
    );
  });

  it('syncs after key changes with the already-validated Razorpay provider', async () => {
    const provider = {
      syncCatalog: vi.fn().mockResolvedValue({
        account: {
          id: 'acc_123',
          merchantName: 'Example Merchant',
          livemode: false,
        },
        plans: [],
        items: [],
      }),
      listCustomers: vi.fn().mockResolvedValue([]),
      listSubscriptions: vi.fn().mockResolvedValue([]),
      listInvoices: vi.fn().mockResolvedValue([]),
      listPayments: vi.fn().mockResolvedValue([]),
    };

    const result = await RazorpaySyncService.getInstance().syncEnvironmentAfterKeyChange(
      'test',
      provider as unknown as RazorpayProvider
    );

    expect(mockConfigService.createRazorpayProvider).not.toHaveBeenCalled();
    expect(provider.syncCatalog).toHaveBeenCalled();
    expect(mockConfigService.writeSnapshot).toHaveBeenCalledWith(
      'test',
      'acc_123',
      'Example Merchant',
      false,
      {
        plans: 0,
        items: 0,
        customers: 0,
        subscriptions: 0,
        invoices: 0,
        payments: 0,
      },
      expect.any(Date)
    );
    expect(result).toEqual(
      expect.objectContaining({
        environment: 'test',
        status: 'succeeded',
        syncCounts: {
          plans: 0,
          items: 0,
          customers: 0,
          subscriptions: 0,
          invoices: 0,
          payments: 0,
        },
        error: null,
      })
    );
  });

  it('stores Razorpay catalog data in provider-native item and plan tables', async () => {
    const mockClient = {
      query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
      release: vi.fn(),
    };
    mockPool.connect.mockResolvedValue(mockClient);
    const provider = {
      syncCatalog: vi.fn().mockResolvedValue({
        account: {
          id: 'acc_123',
          merchantName: 'Example Merchant',
          livemode: false,
        },
        plans: [
          {
            id: 'plan_123',
            entity: 'plan',
            interval: 1,
            period: 'monthly',
            item: {
              id: 'item_123',
              name: 'Pro monthly',
              description: 'Monthly plan',
              amount: 290000,
              unit_amount: 290000,
              currency: 'INR',
              active: true,
            },
            notes: { tier: 'pro' },
            created_at: 1777248000,
          },
        ],
        items: [
          {
            id: 'item_123',
            active: true,
            amount: 290000,
            unit_amount: 290000,
            currency: 'INR',
            name: 'Pro monthly',
            description: 'Monthly plan',
            type: 'invoice',
            created_at: 1777248000,
          },
        ],
      }),
      listCustomers: vi.fn().mockResolvedValue([]),
      listSubscriptions: vi.fn().mockResolvedValue([]),
      listInvoices: vi.fn().mockResolvedValue([]),
      listPayments: vi.fn().mockResolvedValue([]),
    };
    mockConfigService.createRazorpayProvider.mockResolvedValue(provider);

    await RazorpaySyncService.getInstance().syncAll('test');

    const executedSql = mockClient.query.mock.calls.map(([sql]) => String(sql)).join('\n');
    expect(executedSql).toMatch(/INSERT INTO payments\.razorpay_items/i);
    expect(executedSql).toMatch(/INSERT INTO payments\.razorpay_plans/i);
    expect(executedSql).not.toMatch(/INSERT INTO payments\.products/i);
    expect(executedSql).not.toMatch(/INSERT INTO payments\.prices/i);
    expect(executedSql).not.toMatch(/INSERT INTO payments\.subscription_items/i);
  });

  it('syncs Razorpay invoices before payments so transaction context is provider-native', async () => {
    const mockClient = {
      query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
      release: vi.fn(),
    };
    mockPool.connect.mockResolvedValue(mockClient);
    const provider = {
      syncCatalog: vi.fn().mockResolvedValue({
        account: {
          id: 'acc_123',
          merchantName: 'Example Merchant',
          livemode: false,
        },
        plans: [],
        items: [],
      }),
      listCustomers: vi.fn().mockResolvedValue([]),
      listSubscriptions: vi.fn().mockResolvedValue([]),
      listInvoices: vi.fn().mockResolvedValue([
        {
          id: 'inv_123',
          entity: 'invoice',
          type: 'invoice',
          description: 'Pro monthly',
          customer_id: 'cust_123',
          customer_details: {
            id: 'cust_123',
            name: 'Buyer',
            email: 'buyer@example.com',
            contact: null,
          },
          order_id: 'order_123',
          subscription_id: 'sub_123',
          payment_id: 'pay_123',
          status: 'paid',
          amount: 5000,
          amount_paid: 5000,
          amount_due: 0,
          currency: 'INR',
          short_url: null,
          notes: {
            insforge_subject_type: 'team',
            insforge_subject_id: 'team_123',
          },
          line_items: [],
          paid_at: 1780617600,
          cancelled_at: null,
          expired_at: null,
          issued_at: 1780531200,
          created_at: 1780531200,
        },
      ]),
      listPayments: vi.fn().mockResolvedValue([]),
    };
    mockConfigService.createRazorpayProvider.mockResolvedValue(provider);

    await RazorpaySyncService.getInstance().syncAll('test');

    expect(provider.listInvoices.mock.invocationCallOrder[0]).toBeLessThan(
      provider.listPayments.mock.invocationCallOrder[0]
    );
    expect(mockConfigService.writeSnapshot).toHaveBeenCalledWith(
      'test',
      'acc_123',
      'Example Merchant',
      false,
      {
        plans: 0,
        items: 0,
        customers: 0,
        subscriptions: 0,
        invoices: 1,
        payments: 0,
      },
      expect.any(Date)
    );

    const transactionCall = mockClient.query.mock.calls.find(([sql]) =>
      /WITH refs[\s\S]*INSERT INTO payments\.transactions/i.test(String(sql))
    );
    expect(transactionCall).toBeDefined();
    expect(String(transactionCall?.[0])).toMatch(/RETURNING\s+tx\.id/i);

    const params = transactionCall?.[1] as unknown[];
    expect(params).toEqual(
      expect.arrayContaining([
        'test',
        'payment',
        'pay_123',
        'subscription_invoice',
        'succeeded',
        'team',
        'team_123',
        'cust_123',
        'buyer@example.com',
      ])
    );
    expect(JSON.parse(String(params[11]))).toEqual(
      expect.objectContaining({
        payment: 'pay_123',
        invoice: 'inv_123',
        order: 'order_123',
        subscription: 'sub_123',
      })
    );
  });

  it('stores Razorpay subscriptions in the provider-native subscription table', async () => {
    const mockClient = {
      query: vi.fn().mockResolvedValue({ rows: [], rowCount: 0 }),
      release: vi.fn(),
    };
    mockPool.connect.mockResolvedValue(mockClient);
    const provider = {
      syncCatalog: vi.fn().mockResolvedValue({
        account: {
          id: 'acc_123',
          merchantName: 'Example Merchant',
          livemode: false,
        },
        plans: [],
        items: [],
      }),
      listCustomers: vi.fn().mockResolvedValue([]),
      listSubscriptions: vi.fn().mockResolvedValue([
        {
          id: 'sub_123',
          entity: 'subscription',
          plan_id: 'plan_123',
          customer_id: 'cust_123',
          status: 'active',
          current_start: 1777248000,
          current_end: 1779840000,
          ended_at: null,
          quantity: 2,
          notes: { insforge_subject_type: 'team', insforge_subject_id: 'team_123' },
          charge_at: 1779840000,
          start_at: 1777248000,
          end_at: null,
          total_count: 12,
          auth_attempts: 1,
          paid_count: 1,
          remaining_count: 11,
          short_url: 'https://rzp.io/i/sub_123',
          has_scheduled_changes: false,
          change_scheduled_at: null,
          offer_id: null,
          created_at: 1777248000,
        },
      ]),
      listInvoices: vi.fn().mockResolvedValue([]),
      listPayments: vi.fn().mockResolvedValue([]),
    };
    mockConfigService.createRazorpayProvider.mockResolvedValue(provider);

    await RazorpaySyncService.getInstance().syncAll('test');

    expect(mockClient.query).toHaveBeenCalledWith(
      expect.stringMatching(/INSERT INTO payments\.razorpay_subscriptions/i),
      expect.arrayContaining([
        'test',
        'sub_123',
        'plan_123',
        'cust_123',
        'team',
        'team_123',
        'active',
      ])
    );
    expect(mockClient.query).toHaveBeenCalledWith(
      expect.stringMatching(
        /DELETE FROM payments\.razorpay_subscriptions[\s\S]*synced_at IS NULL OR synced_at < \$3[\s\S]*updated_at < \$3/i
      ),
      ['test', ['sub_123'], expect.any(Date)]
    );
    const subscriptionUpsertCall = mockClient.query.mock.calls.find(([sql]) =>
      /INSERT INTO payments\.razorpay_subscriptions/i.test(String(sql))
    );
    const subscriptionDeleteCall = mockClient.query.mock.calls.find(([sql]) =>
      /DELETE FROM payments\.razorpay_subscriptions/i.test(String(sql))
    );
    expect(subscriptionUpsertCall?.[1]?.[25]).toBe(subscriptionDeleteCall?.[1]?.[2]);

    const executedSql = mockClient.query.mock.calls.map(([sql]) => String(sql)).join('\n');
    expect(executedSql).not.toMatch(/INSERT INTO payments\.subscriptions/i);
  });
});
