import { Test, TestingModule } from '@nestjs/testing';
import { MeteringService } from './metering.service';
import { PrismaService } from '../../prisma/prisma.service';

describe('MeteringService', () => {
  let service: MeteringService;
  let mockPrisma: {
    $queryRawUnsafe: jest.Mock;
    $executeRawUnsafe: jest.Mock;
  };

  const clubId = 'club-001';

  beforeEach(async () => {
    jest.clearAllMocks();

    mockPrisma = {
      $queryRawUnsafe: jest.fn(),
      $executeRawUnsafe: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        MeteringService,
        {
          provide: PrismaService,
          useValue: mockPrisma,
        },
      ],
    }).compile();

    service = module.get<MeteringService>(MeteringService);
  });

  describe('aggregateUsage', () => {
    it('should count all resource types and upsert the usage record', async () => {
      const mockUsageRecord = {
        id: 'usage-1',
        clubId,
        period: new Date(),
        activeUsers: 5,
        clients: 100,
        callStorageGb: 0,
        webhooksSent: 320,
        pushSent: 150,
        apiRequests: 5000,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockPrisma.$queryRawUnsafe.mockImplementation((sql: string) => {
        // The INSERT/upsert into usage_records (uses $queryRawUnsafe with RETURNING)
        if (sql.includes('INSERT INTO usage_records')) {
          return Promise.resolve([mockUsageRecord]);
        }
        // countActiveUsers: SELECT COUNT(*) FROM users
        if (sql.includes('FROM users')) {
          return Promise.resolve([{ count: BigInt(5) }]);
        }
        // countClients: SELECT COUNT(*) FROM clients
        if (sql.includes('FROM clients')) {
          return Promise.resolve([{ count: BigInt(100) }]);
        }
        // countWebhooksSent: SELECT COUNT(*) FROM webhook_logs
        if (sql.includes('webhook_logs')) {
          return Promise.resolve([{ count: BigInt(320) }]);
        }
        // countPushSent: SELECT COUNT(*) FROM notifications
        if (sql.includes('FROM notifications')) {
          return Promise.resolve([{ count: BigInt(150) }]);
        }
        // countApiRequests: SELECT ... FROM usage_records (a SELECT, not INSERT)
        if (sql.includes('usage_records') && sql.includes('SELECT')) {
          return Promise.resolve([{ apiRequests: 5000 }]);
        }
        return Promise.resolve([{ count: BigInt(0) }]);
      });

      await service.aggregateUsage(clubId);

      expect(mockPrisma.$queryRawUnsafe).toHaveBeenCalled();

      // The upsert uses $queryRawUnsafe (not $executeRawUnsafe) because of RETURNING
      const upsertCall = mockPrisma.$queryRawUnsafe.mock.calls.find(
        (call: string[]) =>
          call[0].includes('INSERT INTO usage_records'),
      );
      expect(upsertCall).toBeDefined();
    });

    it('should pass the correct clubId to all count queries', async () => {
      const mockUsageRecord = {
        id: 'usage-1',
        clubId,
        period: new Date(),
        activeUsers: 0,
        clients: 0,
        callStorageGb: 0,
        webhooksSent: 0,
        pushSent: 0,
        apiRequests: 0,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockPrisma.$queryRawUnsafe.mockImplementation((sql: string) => {
        if (sql.includes('INSERT INTO usage_records')) {
          return Promise.resolve([mockUsageRecord]);
        }
        if (sql.includes('usage_records') && sql.includes('SELECT')) {
          return Promise.resolve([{ apiRequests: 0 }]);
        }
        return Promise.resolve([{ count: BigInt(0) }]);
      });

      await service.aggregateUsage(clubId);

      const queryCalls = mockPrisma.$queryRawUnsafe.mock.calls;
      for (const call of queryCalls) {
        // Every query should have clubId as one of its parameters
        // (except the INSERT which also has clubId as $1)
        expect(call.slice(1)).toEqual(
          expect.arrayContaining([expect.anything()]),
        );
      }
    });
  });

  describe('getUsage', () => {
    it('should return usage record when it exists', async () => {
      const usageRecord = {
        clubId,
        activeUsers: 5,
        clients: 100,
        callStorageGb: 0,
        webhooksSent: 320,
        pushSent: 150,
        apiRequests: 5000,
        period: new Date('2026-02-01'),
      };
      mockPrisma.$queryRawUnsafe.mockResolvedValue([usageRecord]);

      const result = await service.getUsage(clubId);

      expect(result).toEqual(usageRecord);
      expect(mockPrisma.$queryRawUnsafe).toHaveBeenCalledWith(
        expect.stringContaining('usage_records'),
        clubId,
        expect.anything(),
      );
    });

    it('should return null when no usage record exists', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([]);

      const result = await service.getUsage(clubId);

      expect(result).toBeNull();
    });

    it('should accept an optional period parameter', async () => {
      mockPrisma.$queryRawUnsafe.mockResolvedValue([]);

      const period = new Date('2026-01-01');
      await service.getUsage(clubId, period);

      expect(mockPrisma.$queryRawUnsafe).toHaveBeenCalledWith(
        expect.any(String),
        clubId,
        period,
      );
    });
  });

  describe('checkLimits', () => {
    it('should return exceeded flags when usage exceeds limits', async () => {
      mockPrisma.$queryRawUnsafe.mockImplementation((sql: string) => {
        // getUsage SELECT from usage_records
        if (sql.includes('usage_records') && sql.includes('SELECT') && !sql.includes('INSERT')) {
          return Promise.resolve([
            {
              id: 'usage-1',
              clubId,
              activeUsers: 15,
              clients: 1500,
              callStorageGb: 5,
              webhooksSent: 12000,
              pushSent: 6000,
              apiRequests: 60000,
            },
          ]);
        }
        // licenses query
        if (sql.includes('licenses')) {
          return Promise.resolve([
            {
              maxUsers: 10,
              maxClients: 1000,
            },
          ]);
        }
        // club_modules query
        if (sql.includes('club_modules')) {
          return Promise.resolve([
            {
              moduleId: 'webhooks',
              limitsJson: { max_events: 10000 },
            },
            {
              moduleId: 'push',
              limitsJson: { max_messages: 5000 },
            },
            {
              moduleId: 'sip_recording',
              limitsJson: { max_storage_gb: 2 },
            },
          ]);
        }
        return Promise.resolve([]);
      });

      const result = await service.checkLimits(clubId);

      expect(Array.isArray(result)).toBe(true);
      expect(result.length).toBeGreaterThan(0);

      const exceededResources = result.filter((check: any) => check.exceeded);
      expect(exceededResources.length).toBeGreaterThan(0);
    });

    it('should return empty array when no license is found', async () => {
      mockPrisma.$queryRawUnsafe.mockImplementation((sql: string) => {
        if (sql.includes('usage_records') && sql.includes('SELECT') && !sql.includes('INSERT')) {
          return Promise.resolve([
            {
              id: 'usage-1',
              clubId,
              activeUsers: 5,
              clients: 100,
              callStorageGb: 0,
              webhooksSent: 100,
              pushSent: 50,
              apiRequests: 1000,
            },
          ]);
        }
        if (sql.includes('licenses')) {
          return Promise.resolve([]);
        }
        if (sql.includes('club_modules')) {
          return Promise.resolve([]);
        }
        return Promise.resolve([]);
      });

      const result = await service.checkLimits(clubId);

      expect(Array.isArray(result)).toBe(true);
      expect(result).toEqual([]);
    });

    it('should return no exceeded flags when usage is within limits', async () => {
      mockPrisma.$queryRawUnsafe.mockImplementation((sql: string) => {
        if (sql.includes('usage_records') && sql.includes('SELECT') && !sql.includes('INSERT')) {
          return Promise.resolve([
            {
              id: 'usage-1',
              clubId,
              activeUsers: 5,
              clients: 500,
              callStorageGb: 0,
              webhooksSent: 3000,
              pushSent: 2000,
              apiRequests: 20000,
            },
          ]);
        }
        if (sql.includes('licenses')) {
          return Promise.resolve([
            {
              maxUsers: 10,
              maxClients: 1000,
            },
          ]);
        }
        if (sql.includes('club_modules')) {
          return Promise.resolve([
            {
              moduleId: 'webhooks',
              limitsJson: { max_events: 10000 },
            },
            {
              moduleId: 'push',
              limitsJson: { max_messages: 5000 },
            },
            {
              moduleId: 'sip_recording',
              limitsJson: { max_storage_gb: 0 },
            },
          ]);
        }
        return Promise.resolve([]);
      });

      const result = await service.checkLimits(clubId);

      const exceededResources = result.filter((check: any) => check.exceeded);
      expect(exceededResources).toEqual([]);
    });
  });

  describe('recordApiRequest', () => {
    it('should execute an upsert SQL to increment the apiRequests counter', async () => {
      mockPrisma.$executeRawUnsafe.mockResolvedValue(1);

      await service.recordApiRequest(clubId);

      expect(mockPrisma.$executeRawUnsafe).toHaveBeenCalledTimes(1);
      const sql = mockPrisma.$executeRawUnsafe.mock.calls[0][0];
      expect(sql).toMatch(/INSERT|UPDATE|UPSERT|api/i);
      expect(mockPrisma.$executeRawUnsafe.mock.calls[0]).toContain(clubId);
    });
  });

  describe('recordWebhookSent', () => {
    it('should execute an upsert SQL to increment the webhooksSent counter', async () => {
      mockPrisma.$executeRawUnsafe.mockResolvedValue(1);

      await service.recordWebhookSent(clubId);

      expect(mockPrisma.$executeRawUnsafe).toHaveBeenCalledTimes(1);
      const sql = mockPrisma.$executeRawUnsafe.mock.calls[0][0];
      expect(sql).toMatch(/INSERT|UPDATE|UPSERT|webhook/i);
      expect(mockPrisma.$executeRawUnsafe.mock.calls[0]).toContain(clubId);
    });
  });

  describe('recordPushSent', () => {
    it('should execute an upsert SQL to increment the pushSent counter', async () => {
      mockPrisma.$executeRawUnsafe.mockResolvedValue(1);

      await service.recordPushSent(clubId);

      expect(mockPrisma.$executeRawUnsafe).toHaveBeenCalledTimes(1);
      const sql = mockPrisma.$executeRawUnsafe.mock.calls[0][0];
      expect(sql).toMatch(/INSERT|UPDATE|UPSERT|push/i);
      expect(mockPrisma.$executeRawUnsafe.mock.calls[0]).toContain(clubId);
    });
  });
});
