/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ExportResultCode } from '@opentelemetry/core';
import type { ReadableLogRecord } from '@opentelemetry/sdk-logs';
import {
  GcpTraceExporter,
  GcpMetricExporter,
  GcpLogExporter,
} from './gcp-exporters.js';

const mockLogEntry = { test: 'entry' };
const mockLogWrite = vi.fn().mockResolvedValue(undefined);
const mockLog = {
  entry: vi.fn().mockReturnValue(mockLogEntry),
  write: mockLogWrite,
};
const mockLogging = {
  projectId: 'test-project',
  log: vi.fn().mockReturnValue(mockLog),
};

vi.mock('@google-cloud/opentelemetry-cloud-trace-exporter', () => ({
  TraceExporter: vi.fn().mockImplementation(() => ({
    export: vi.fn(),
    shutdown: vi.fn(),
    forceFlush: vi.fn(),
  })),
}));

vi.mock('@google-cloud/opentelemetry-cloud-monitoring-exporter', () => ({
  MetricExporter: vi.fn().mockImplementation(() => ({
    export: vi.fn(),
    shutdown: vi.fn(),
    forceFlush: vi.fn(),
  })),
}));

vi.mock('@google-cloud/logging', () => ({
  Logging: vi.fn().mockImplementation(() => mockLogging),
}));

describe('GCP Exporters', () => {
  describe('GcpTraceExporter', () => {
    it('should create a trace exporter with correct configuration', () => {
      const exporter = new GcpTraceExporter('test-project');
      expect(exporter).toBeDefined();
    });

    it('should create a trace exporter without project ID', () => {
      const exporter = new GcpTraceExporter();
      expect(exporter).toBeDefined();
    });
  });

  describe('GcpMetricExporter', () => {
    it('should create a metric exporter with correct configuration', () => {
      const exporter = new GcpMetricExporter('test-project');
      expect(exporter).toBeDefined();
    });

    it('should create a metric exporter without project ID', () => {
      const exporter = new GcpMetricExporter();
      expect(exporter).toBeDefined();
    });
  });

  describe('GcpLogExporter', () => {
    let exporter: GcpLogExporter;

    beforeEach(() => {
      vi.clearAllMocks();
      mockLogWrite.mockResolvedValue(undefined);
      mockLog.entry.mockReturnValue(mockLogEntry);
      exporter = new GcpLogExporter('test-project');
    });

    describe('constructor', () => {
      it('should create a log exporter with project ID', () => {
        expect(exporter).toBeDefined();
        expect(mockLogging.log).toHaveBeenCalledWith('gemini_cli');
      });

      it('should create a log exporter without project ID', () => {
        const exporterNoProject = new GcpLogExporter();
        expect(exporterNoProject).toBeDefined();
      });
    });

    describe('export', () => {
      it('should export logs successfully', async () => {
        const mockLogRecords: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            severityNumber: 9,
            severityText: 'INFO',
            body: 'Test log message',
            attributes: {
              'session.id': 'test-session',
              'custom.attribute': 'value',
            },
            resource: {
              attributes: {
                'service.name': 'test-service',
              },
            },
          } as unknown as ReadableLogRecord,
        ];

        const callback = vi.fn();

        exporter.export(mockLogRecords, callback);

        await new Promise((resolve) => setTimeout(resolve, 0));

        expect(mockLog.entry).toHaveBeenCalledWith(
          expect.objectContaining({
            severity: 'INFO',
            timestamp: expect.any(Date),
            resource: {
              type: 'global',
              labels: {
                project_id: 'test-project',
              },
            },
          }),
          expect.objectContaining({
            message: 'Test log message',
            'session.id': 'test-session',
            'custom.attribute': 'value',
            'service.name': 'test-service',
          }),
        );

        expect(mockLog.write).toHaveBeenCalledWith([mockLogEntry]);
        expect(callback).toHaveBeenCalledWith({
          code: ExportResultCode.SUCCESS,
        });
      });

      it('should truncate payload strictly if GEMINI_STRICT_TELEMETRY_LIMITS is true', async () => {
        vi.stubEnv('GEMINI_STRICT_TELEMETRY_LIMITS', 'true');

        // Create an array of 60 strings, each 10,000 characters long.
        // Even after the 2k strict truncation pass, the total size will be
        // ~120k, which forces the final fallback structural strip.
        const largeArray = Array(60).fill('a'.repeat(10000));

        const mockLogRecords: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            severityNumber: 9,
            body: 'Test',
            attributes: {
              huge_data: largeArray,
            },
          } as unknown as ReadableLogRecord,
        ];

        const callback = vi.fn();
        exporter.export(mockLogRecords, callback);

        await new Promise((resolve) => setTimeout(resolve, 0));

        expect(mockLog.entry).toHaveBeenCalled();
        const entryCallArgs = mockLog.entry.mock.calls[0];
        const payload = entryCallArgs[1];

        // Should have fallen back to structural strip due to strict limit
        expect(payload).toHaveProperty(
          '_warning',
          'Payload heavily truncated due to strict limits',
        );
        expect(payload.data.length).toBeLessThanOrEqual(50050); // 50000 + '... (truncated)'

        vi.unstubAllEnvs();
      });

      it('should completely bypass truncation if GEMINI_STRICT_TELEMETRY_LIMITS is false or unset', async () => {
        const largeArray = Array(60).fill('a'.repeat(10000));

        const mockLogRecords: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            severityNumber: 9,
            body: 'Test',
            attributes: {
              huge_data: largeArray,
            },
          } as unknown as ReadableLogRecord,
        ];

        const callback = vi.fn();
        exporter.export(mockLogRecords, callback);

        await new Promise((resolve) => setTimeout(resolve, 0));

        expect(mockLog.entry).toHaveBeenCalled();
        const entryCallArgs = mockLog.entry.mock.calls[0];
        const payload = entryCallArgs[1];

        // Should NOT have fallen back to structural strip, array should be intact
        expect(payload).not.toHaveProperty('_warning');
        expect(payload).toHaveProperty('huge_data');
        expect(payload.huge_data).toEqual(largeArray);
      });

      it('should handle export failures', async () => {
        const mockLogRecords: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            body: 'Test log message',
          } as unknown as ReadableLogRecord,
        ];

        const error = new Error('Write failed');
        mockLogWrite.mockRejectedValueOnce(error);

        const callback = vi.fn();

        exporter.export(mockLogRecords, callback);

        await new Promise((resolve) => setTimeout(resolve, 0));

        expect(callback).toHaveBeenCalledWith({
          code: ExportResultCode.FAILED,
          error,
        });
      });

      it('should handle synchronous errors', () => {
        const mockLogRecords: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            body: 'Test log message',
          } as unknown as ReadableLogRecord,
        ];

        mockLog.entry.mockImplementation(() => {
          throw new Error('Entry creation failed');
        });

        const callback = vi.fn();

        exporter.export(mockLogRecords, callback);

        expect(callback).toHaveBeenCalledWith({
          code: ExportResultCode.FAILED,
          error: expect.any(Error),
        });
      });
    });

    describe('severity mapping', () => {
      it('should map OpenTelemetry severity numbers to Cloud Logging levels', () => {
        const testCases = [
          { severityNumber: undefined, expected: 'DEFAULT' },
          { severityNumber: 1, expected: 'DEFAULT' },
          { severityNumber: 5, expected: 'DEBUG' },
          { severityNumber: 9, expected: 'INFO' },
          { severityNumber: 13, expected: 'WARNING' },
          { severityNumber: 17, expected: 'ERROR' },
          { severityNumber: 21, expected: 'CRITICAL' },
          { severityNumber: 25, expected: 'CRITICAL' },
        ];

        testCases.forEach(({ severityNumber, expected }) => {
          const mockLogRecords: ReadableLogRecord[] = [
            {
              hrTime: [1234567890, 123456789],
              hrTimeObserved: [1234567890, 123456789],
              severityNumber,
              body: 'Test message',
            } as unknown as ReadableLogRecord,
          ];

          const callback = vi.fn();
          exporter.export(mockLogRecords, callback);

          expect(mockLog.entry).toHaveBeenCalledWith(
            expect.objectContaining({
              severity: expected,
            }),
            expect.any(Object),
          );

          mockLog.entry.mockClear();
        });
      });
    });

    describe('forceFlush', () => {
      it('should resolve immediately when no pending writes exist', async () => {
        await expect(exporter.forceFlush()).resolves.toBeUndefined();
      });

      it('should wait for pending writes to complete', async () => {
        const mockLogRecords: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            body: 'Test log message',
          } as unknown as ReadableLogRecord,
        ];

        let resolveWrite: () => void;
        const writePromise = new Promise<void>((resolve) => {
          resolveWrite = resolve;
        });
        mockLogWrite.mockReturnValueOnce(writePromise);

        const callback = vi.fn();

        exporter.export(mockLogRecords, callback);
        const flushPromise = exporter.forceFlush();

        await new Promise((resolve) => setTimeout(resolve, 1));

        resolveWrite!();
        await writePromise;

        await expect(flushPromise).resolves.toBeUndefined();
      });

      it('should handle multiple pending writes', async () => {
        const mockLogRecords1: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            body: 'Test log message 1',
          } as unknown as ReadableLogRecord,
        ];

        const mockLogRecords2: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            body: 'Test log message 2',
          } as unknown as ReadableLogRecord,
        ];

        let resolveWrite1: () => void;
        let resolveWrite2: () => void;
        const writePromise1 = new Promise<void>((resolve) => {
          resolveWrite1 = resolve;
        });
        const writePromise2 = new Promise<void>((resolve) => {
          resolveWrite2 = resolve;
        });

        mockLogWrite
          .mockReturnValueOnce(writePromise1)
          .mockReturnValueOnce(writePromise2);

        const callback = vi.fn();

        exporter.export(mockLogRecords1, callback);
        exporter.export(mockLogRecords2, callback);

        const flushPromise = exporter.forceFlush();

        resolveWrite1!();
        await writePromise1;

        resolveWrite2!();
        await writePromise2;

        await expect(flushPromise).resolves.toBeUndefined();
      });

      it('should handle write failures gracefully', async () => {
        const mockLogRecords: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            body: 'Test log message',
          } as unknown as ReadableLogRecord,
        ];

        const error = new Error('Write failed');
        mockLogWrite.mockRejectedValueOnce(error);

        const callback = vi.fn();

        exporter.export(mockLogRecords, callback);

        await expect(exporter.forceFlush()).resolves.toBeUndefined();

        await new Promise((resolve) => setTimeout(resolve, 10));
        expect(callback).toHaveBeenCalledWith({
          code: ExportResultCode.FAILED,
          error,
        });
      });
    });

    describe('shutdown', () => {
      it('should call forceFlush', async () => {
        const forceFlushSpy = vi.spyOn(exporter, 'forceFlush');

        await exporter.shutdown();

        expect(forceFlushSpy).toHaveBeenCalled();
      });

      it('should handle shutdown gracefully', async () => {
        const forceFlushSpy = vi.spyOn(exporter, 'forceFlush');

        await expect(exporter.shutdown()).resolves.toBeUndefined();
        expect(forceFlushSpy).toHaveBeenCalled();
      });
      it('should wait for pending writes before shutting down', async () => {
        const mockLogRecords: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            body: 'Test log message',
          } as unknown as ReadableLogRecord,
        ];

        let resolveWrite: () => void;
        const writePromise = new Promise<void>((resolve) => {
          resolveWrite = resolve;
        });
        mockLogWrite.mockReturnValueOnce(writePromise);

        const callback = vi.fn();

        exporter.export(mockLogRecords, callback);
        const shutdownPromise = exporter.shutdown();

        await new Promise((resolve) => setTimeout(resolve, 1));

        resolveWrite!();
        await writePromise;

        await expect(shutdownPromise).resolves.toBeUndefined();
      });

      it('should clear pending writes array after shutdown', async () => {
        const mockLogRecords: ReadableLogRecord[] = [
          {
            hrTime: [1234567890, 123456789],
            hrTimeObserved: [1234567890, 123456789],
            body: 'Test log message',
          } as unknown as ReadableLogRecord,
        ];

        const callback = vi.fn();

        exporter.export(mockLogRecords, callback);

        await new Promise((resolve) => setTimeout(resolve, 10));

        await exporter.shutdown();

        const start = Date.now();
        await exporter.forceFlush();
        const elapsed = Date.now() - start;

        expect(elapsed).toBeLessThan(50);
      });
    });
  });
});
