/**
 * Unit tests for src/services/BillingService.ts and src/services/UsageService.ts
 */

import { Pool, QueryResult } from 'pg';
import Stripe from 'stripe';
import { BillingService } from '../../../src/services/BillingService';
import { UsageService } from '../../../src/services/UsageService';

// ── Mock pg Pool ─────────────────────────────────────────────────────────────

function makePool(queryFn: jest.Mock): Pool {
  return { query: queryFn } as unknown as Pool;
}

// ── Mock Stripe ───────────────────────────────────────────────────────────────

function makeStripe(overrides: Partial<{
  checkoutUrl: string;
  constructEvent: () => Stripe.Event;
  retrieveCustomer: () => Stripe.Customer;
}> = {}): Stripe {
  const checkoutUrl = overrides.checkoutUrl ?? 'https://checkout.stripe.com/session_test';

  const fakeSubscription: Stripe.Subscription = {
    id: 'sub_1',
    object: 'subscription',
    status: 'active',
    customer: 'cus_1',
    billing_cycle_anchor: Math.floor(Date.now() / 1000) + 86400,
    ended_at: null,
  } as unknown as Stripe.Subscription;

  const fakeEvent: Stripe.Event = {
    id: 'evt_1',
    type: 'customer.subscription.created',
    object: 'event',
    data: { object: fakeSubscription },
    api_version: '2026-03-25.dahlia',
    created: Math.floor(Date.now() / 1000),
    livemode: false,
    pending_webhooks: 0,
    request: null,
  } as unknown as Stripe.Event;

  const fakeCustomer: Stripe.Customer = {
    id: 'cus_1',
    object: 'customer',
    created: Math.floor(Date.now() / 1000),
    livemode: false,
    metadata: { tenant_id: 'tenant-uuid-123' },
    deleted: undefined,
  } as unknown as Stripe.Customer;

  return {
    checkout: {
      sessions: {
        create: jest.fn().mockResolvedValue({ url: checkoutUrl, id: 'cs_1' }),
      },
    },
    webhooks: {
      constructEvent: overrides.constructEvent
        ? jest.fn(overrides.constructEvent)
        : jest.fn().mockReturnValue(fakeEvent),
    },
    customers: {
      retrieve: overrides.retrieveCustomer
        ? jest.fn(overrides.retrieveCustomer)
        : jest.fn().mockResolvedValue(fakeCustomer),
    },
  } as unknown as Stripe;
}

// ════════════════════════════════════════════════════════════════════════════
// UsageService
// ════════════════════════════════════════════════════════════════════════════

describe('UsageService', () => {
  describe('getDailyUsage()', () => {
    it('should return correct IUsageSummary with api_calls from DB', async () => {
      const mockQuery = jest.fn()
        .mockResolvedValueOnce({ rows: [{ count: '42' }] } as unknown as QueryResult)
        .mockResolvedValueOnce({ rows: [{ count: '5' }] } as unknown as QueryResult);

      const service = new UsageService(makePool(mockQuery));
      const result = await service.getDailyUsage('tenant-1', '2026-04-02');

      expect(result).toEqual({
        tenantId: 'tenant-1',
        date: '2026-04-02',
        apiCalls: 42,
        agentCount: 5,
      });
    });

    it('should default apiCalls to 0 when no usage_events row exists', async () => {
      const mockQuery = jest.fn()
        .mockResolvedValueOnce({ rows: [{ count: '0' }] } as unknown as QueryResult)
        .mockResolvedValueOnce({ rows: [{ count: '2' }] } as unknown as QueryResult);

      const service = new UsageService(makePool(mockQuery));
      const result = await service.getDailyUsage('tenant-2', '2026-04-02');

      expect(result.apiCalls).toBe(0);
      expect(result.agentCount).toBe(2);
    });
  });

  describe('getActiveAgentCount()', () => {
    it('should return the count of non-decommissioned agents', async () => {
      const mockQuery = jest.fn().mockResolvedValue({
        rows: [{ count: '7' }],
      } as unknown as QueryResult);

      const service = new UsageService(makePool(mockQuery));
      const count = await service.getActiveAgentCount('tenant-1');

      expect(count).toBe(7);
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining("status != 'decommissioned'"),
        ['tenant-1'],
      );
    });

    it('should return 0 when no agents exist', async () => {
      const mockQuery = jest.fn().mockResolvedValue({
        rows: [{ count: '0' }],
      } as unknown as QueryResult);

      const service = new UsageService(makePool(mockQuery));
      const count = await service.getActiveAgentCount('tenant-empty');

      expect(count).toBe(0);
    });
  });
});

// ════════════════════════════════════════════════════════════════════════════
// BillingService
// ════════════════════════════════════════════════════════════════════════════

describe('BillingService', () => {
  describe('createCheckoutSession()', () => {
    it('should return the Stripe checkout URL', async () => {
      const mockQuery = jest.fn();
      const stripe = makeStripe({ checkoutUrl: 'https://checkout.stripe.com/test' });
      const service = new BillingService(makePool(mockQuery), stripe);

      const url = await service.createCheckoutSession(
        'tenant-1',
        'https://app.com/success',
        'https://app.com/cancel',
      );

      expect(url).toBe('https://checkout.stripe.com/test');
    });
  });

  describe('getSubscriptionStatus()', () => {
    it('should return free status when no subscription row exists', async () => {
      const mockQuery = jest.fn().mockResolvedValue({ rows: [] } as unknown as QueryResult);
      const stripe = makeStripe();
      const service = new BillingService(makePool(mockQuery), stripe);

      const status = await service.getSubscriptionStatus('tenant-free');

      expect(status).toEqual({
        tenantId: 'tenant-free',
        status: 'free',
        currentPeriodEnd: null,
        stripeSubscriptionId: null,
      });
    });

    it('should return subscription data when a row exists', async () => {
      const periodEnd = new Date('2026-05-01T00:00:00Z');
      const mockQuery = jest.fn().mockResolvedValue({
        rows: [{
          status: 'active',
          current_period_end: periodEnd,
          stripe_subscription_id: 'sub_abc123',
        }],
      } as unknown as QueryResult);

      const stripe = makeStripe();
      const service = new BillingService(makePool(mockQuery), stripe);

      const status = await service.getSubscriptionStatus('tenant-paid');

      expect(status.status).toBe('active');
      expect(status.stripeSubscriptionId).toBe('sub_abc123');
      expect(status.currentPeriodEnd).toEqual(periodEnd);
    });
  });

  describe('handleWebhookEvent()', () => {
    it('should process customer.subscription.created and upsert DB', async () => {
      const mockQuery = jest.fn().mockResolvedValue({ rows: [] } as unknown as QueryResult);
      const stripe = makeStripe();
      const service = new BillingService(makePool(mockQuery), stripe);

      await service.handleWebhookEvent(
        Buffer.from('{}'),
        'stripe-sig-header',
        'whsec_test',
      );

      // constructEvent should have been called with raw body, sig, and secret
      expect(stripe.webhooks.constructEvent).toHaveBeenCalledWith(
        expect.any(Buffer),
        'stripe-sig-header',
        'whsec_test',
      );

      // DB upsert should have been called
      expect(mockQuery).toHaveBeenCalledWith(
        expect.stringContaining('INSERT INTO tenant_subscriptions'),
        expect.any(Array),
      );
    });

    it('should throw when Stripe signature verification fails', async () => {
      const mockQuery = jest.fn();
      const sigError = new Error('Stripe signature verification failed');
      const stripe = makeStripe({
        constructEvent: () => { throw sigError; },
      });

      const service = new BillingService(makePool(mockQuery), stripe);

      await expect(
        service.handleWebhookEvent(Buffer.from('{}'), 'bad-sig', 'whsec_test'),
      ).rejects.toThrow('Stripe signature verification failed');
    });

    it('should skip processing for unrecognised event types', async () => {
      const mockQuery = jest.fn();

      const unknownEvent: unknown = {
        id: 'evt_unknown',
        type: 'payment_intent.created',
        object: 'event',
        data: { object: {} },
        api_version: '2026-03-25.dahlia',
        created: Math.floor(Date.now() / 1000),
        livemode: false,
        pending_webhooks: 0,
        request: null,
      };

      const stripe = makeStripe({
        constructEvent: () => unknownEvent as Stripe.Event,
      });

      const service = new BillingService(makePool(mockQuery), stripe);
      await service.handleWebhookEvent(Buffer.from('{}'), 'sig', 'whsec_test');

      // No DB query for unrecognised events
      expect(mockQuery).not.toHaveBeenCalled();
    });
  });
});
