/**
 * @vitest-environment node
 */

import { ErrorCategory } from '../utils/errors/categories.js';
import { MetricThresholdStatus } from '../types/metrics.js';
import { ReportType } from '../deepsource.js';
import {
  AuthError,
  NetworkError,
  ServerError,
  ClientError,
  ApiResponse,
  SuccessResponse,
  ErrorResponse,
  PendingRun,
  RunningRun,
  SuccessfulRun,
  FailedRun,
  TimedOutRun,
  CancelledRun,
  SkippedRun,
  RunState,
  OwaspTop10Report,
  SansTop25Report,
  CodeCoverageReport,
  IssuesPreventedReport,
  ComplianceReport,
  PassingMetric,
  FailingMetric,
  MetricState,
  isErrorOfCategory,
  isSuccessResponse,
  isRunInState,
  isReportOfType,
  isMetricInState,
} from '../types/discriminated-unions.js';
import { asRunId, asBranchName, asCommitOid } from '../types/branded.js';

describe('Discriminated Unions', () => {
  const runId = asRunId('run-123');
  const commitOid = asCommitOid('abcdef123456');
  const branchName = asBranchName('main');

  describe('ApiError Union', () => {
    it('should create different error types with correct discriminants', () => {
      // Auth error
      const authError: AuthError = {
        category: ErrorCategory.AUTH,
        message: 'Invalid API key',
        token: {
          present: true,
          expired: true,
        },
      };

      // Network error
      const networkError: NetworkError = {
        category: ErrorCategory.NETWORK,
        message: 'Connection failed',
        url: 'https://api.deepsource.io',
        connection: {
          established: false,
          timeSpent: 5000,
        },
      };

      // Server error
      const serverError: ServerError = {
        category: ErrorCategory.SERVER,
        message: 'Internal server error',
        statusCode: 500,
        response: { error: 'unexpected error' },
      };

      expect(authError.category).toBe(ErrorCategory.AUTH);
      expect(networkError.category).toBe(ErrorCategory.NETWORK);
      expect(serverError.category).toBe(ErrorCategory.SERVER);

      // Type guard should correctly identify error categories
      expect(isErrorOfCategory(authError, ErrorCategory.AUTH)).toBe(true);
      expect(isErrorOfCategory(networkError, ErrorCategory.AUTH)).toBe(false);
      expect(isErrorOfCategory(serverError, ErrorCategory.SERVER)).toBe(true);
    });

    it('should handle errors with metadata', () => {
      const errorWithMetadata: ClientError = {
        category: ErrorCategory.CLIENT,
        message: 'Validation failed',
        statusCode: 400,
        metadata: {
          requestId: '1234',
          timestamp: new Date().toISOString(),
        },
        validationErrors: {
          field1: 'Required field',
          field2: 'Invalid format',
        },
      };

      expect(errorWithMetadata.metadata).toBeDefined();
      expect(errorWithMetadata.validationErrors).toBeDefined();
      expect(errorWithMetadata.validationErrors?.field1).toBe('Required field');
    });
  });

  describe('ApiResponse Union', () => {
    it('should handle successful responses', () => {
      const successResponse: SuccessResponse<string> = {
        success: true,
        data: 'Response data',
      };

      expect(isSuccessResponse(successResponse)).toBe(true);
      expect(successResponse.data).toBe('Response data');
    });

    it('should handle error responses', () => {
      const errorResponse: ErrorResponse = {
        success: false,
        error: {
          category: ErrorCategory.SERVER,
          message: 'Server error occurred',
        },
      };

      expect(isSuccessResponse(errorResponse)).toBe(false);
      expect(errorResponse.error.message).toBe('Server error occurred');
    });

    it('should discriminate based on success property', () => {
      const handleResponse = (response: ApiResponse<string>): string => {
        if (isSuccessResponse(response)) {
          return `Success: ${response.data}`;
        } else {
          return `Error: ${response.error.message}`;
        }
      };

      const successResponse: SuccessResponse<string> = {
        success: true,
        data: 'Data received',
      };

      const errorResponse: ErrorResponse = {
        success: false,
        error: {
          category: ErrorCategory.NOT_FOUND,
          message: 'Resource not found',
        },
      };

      expect(handleResponse(successResponse)).toBe('Success: Data received');
      expect(handleResponse(errorResponse)).toBe('Error: Resource not found');
    });
  });

  describe('RunState Union', () => {
    it('should create different run states with correct discriminants', () => {
      // Pending run
      const pendingRun: PendingRun = {
        status: 'PENDING',
        runId,
        commitOid,
        branchName,
        baseOid: commitOid,
        createdAt: '2023-05-20T10:00:00Z',
        updatedAt: '2023-05-20T10:00:00Z',
        queuePosition: 3,
        estimatedStartTime: '2023-05-20T10:05:00Z',
      };

      // Running run
      const runningRun: RunningRun = {
        status: 'READY',
        runId,
        commitOid,
        branchName,
        baseOid: commitOid,
        createdAt: '2023-05-20T10:00:00Z',
        updatedAt: '2023-05-20T10:05:00Z',
        progress: 45,
        currentStage: 'analyzing',
        estimatedCompletionTime: '2023-05-20T10:15:00Z',
      };

      // Successful run
      const successfulRun: SuccessfulRun = {
        status: 'SUCCESS',
        runId,
        commitOid,
        branchName,
        baseOid: commitOid,
        createdAt: '2023-05-20T10:00:00Z',
        updatedAt: '2023-05-20T10:15:00Z',
        finishedAt: '2023-05-20T10:15:00Z',
        summary: {
          occurrencesIntroduced: 5,
          occurrencesResolved: 10,
          occurrencesSuppressed: 2,
          analyzerDistribution: [
            { analyzer: 'python', count: 3 },
            { analyzer: 'javascript', count: 2 },
          ],
          categoryDistribution: [
            { category: 'security', count: 1 },
            { category: 'maintainability', count: 4 },
          ],
        },
      };

      expect(pendingRun.status).toBe('PENDING');
      expect(runningRun.status).toBe('READY');
      expect(successfulRun.status).toBe('SUCCESS');

      // Type guard should correctly identify run states
      expect(isRunInState<PendingRun>(pendingRun, 'PENDING')).toBe(true);
      expect(isRunInState<RunningRun>(runningRun, 'READY')).toBe(true);
      expect(isRunInState<SuccessfulRun>(successfulRun, 'SUCCESS')).toBe(true);
      expect(isRunInState<FailedRun>(successfulRun, 'FAILURE')).toBe(false);
    });

    it('should handle run state specific properties', () => {
      // Function that returns different messages based on run state
      const getRunMessage = (run: RunState): string => {
        switch (run.status) {
          case 'PENDING':
            return `Run ${run.runId} is pending in position ${(run as PendingRun).queuePosition || 'unknown'}`;
          case 'READY':
            return `Run ${run.runId} is in progress (${(run as RunningRun).progress || 0}% complete)`;
          case 'SUCCESS':
            return `Run ${run.runId} succeeded with ${(run as SuccessfulRun).summary.occurrencesIntroduced} new issues`;
          case 'FAILURE':
            return `Run ${run.runId} failed: ${(run as FailedRun).error?.message || 'Unknown error'}`;
          case 'TIMEOUT':
            return `Run ${run.runId} timed out after ${(run as TimedOutRun).timeout?.limitSeconds || 'unknown'} seconds`;
          case 'CANCEL':
            return `Run ${run.runId} was cancelled by ${(run as CancelledRun).cancelledBy || 'unknown'}`;
          case 'SKIPPED':
            return `Run ${run.runId} was skipped: ${(run as SkippedRun).skipReason || 'No reason provided'}`;
          default:
            return `Run ${run.runId} has unknown status: ${run.status}`;
        }
      };

      const pendingRun: PendingRun = {
        status: 'PENDING',
        runId,
        commitOid,
        branchName,
        baseOid: commitOid,
        createdAt: '2023-05-20T10:00:00Z',
        updatedAt: '2023-05-20T10:00:00Z',
        queuePosition: 3,
      };

      const failedRun: FailedRun = {
        status: 'FAILURE',
        runId,
        commitOid,
        branchName,
        baseOid: commitOid,
        createdAt: '2023-05-20T10:00:00Z',
        updatedAt: '2023-05-20T10:05:00Z',
        finishedAt: '2023-05-20T10:05:00Z',
        error: {
          message: 'Permission denied',
          code: 'ACCESS_DENIED',
        },
      };

      expect(getRunMessage(pendingRun)).toBe('Run run-123 is pending in position 3');
      expect(getRunMessage(failedRun)).toBe('Run run-123 failed: Permission denied');
    });
  });

  describe('ComplianceReport Union', () => {
    it('should create different report types with correct discriminants', () => {
      // OWASP report
      const owaspReport: OwaspTop10Report = {
        type: ReportType.OWASP_TOP_10,
        title: 'OWASP Top 10',
        currentValue: 85,
        status: 'PASSING',
        categories: [
          {
            key: 'A01:2021',
            title: 'Broken Access Control',
            issues: {
              critical: 0,
              major: 2,
              minor: 1,
              total: 3,
            },
          },
          {
            key: 'A02:2021',
            title: 'Cryptographic Failures',
            issues: {
              critical: 1,
              major: 0,
              minor: 0,
              total: 1,
            },
          },
        ],
      };

      // Code coverage report
      const coverageReport: CodeCoverageReport = {
        type: ReportType.CODE_COVERAGE,
        title: 'Code Coverage',
        currentValue: 78.5,
        status: 'PASSING',
        coverage: {
          line: 80.2,
          branch: 75.6,
          statement: 81.3,
          function: 90.0,
        },
        trend: [
          { date: '2023-04-20', value: 75.0 },
          { date: '2023-05-01', value: 76.2 },
          { date: '2023-05-15', value: 78.5 },
        ],
      };

      expect(owaspReport.type).toBe(ReportType.OWASP_TOP_10);
      expect(coverageReport.type).toBe(ReportType.CODE_COVERAGE);

      // Type guard should correctly identify report types
      expect(isReportOfType<OwaspTop10Report>(owaspReport, ReportType.OWASP_TOP_10)).toBe(true);
      expect(isReportOfType<CodeCoverageReport>(coverageReport, ReportType.CODE_COVERAGE)).toBe(
        true
      );
      expect(isReportOfType<SansTop25Report>(owaspReport, ReportType.SANS_TOP_25)).toBe(false);
    });

    it('should handle report-specific properties', () => {
      // Function that processes report based on type
      const getReportSummary = (report: ComplianceReport): string => {
        switch (report.type) {
          case ReportType.OWASP_TOP_10:
            return `OWASP compliance: ${report.currentValue}%, issues in ${(report as OwaspTop10Report).categories.length} categories`;
          case ReportType.SANS_TOP_25:
            return `SANS compliance: ${report.currentValue}%`;
          case ReportType.MISRA_C:
            return `MISRA-C compliance: ${report.currentValue}%`;
          case ReportType.CODE_COVERAGE:
            return `Code coverage: ${report.currentValue}% (Line: ${(report as CodeCoverageReport).coverage.line}%)`;
          case ReportType.ISSUES_PREVENTED:
            return `Issues prevented: ${(report as IssuesPreventedReport).prevented.total}`;
          default:
            return `Unknown report type: ${report.type}, value: ${report.currentValue}%`;
        }
      };

      const owaspReport: OwaspTop10Report = {
        type: ReportType.OWASP_TOP_10,
        title: 'OWASP Top 10',
        currentValue: 85,
        status: 'PASSING',
        categories: [
          {
            key: 'A01:2021',
            title: 'Broken Access Control',
            issues: {
              critical: 0,
              major: 2,
              minor: 1,
              total: 3,
            },
          },
        ],
      };

      const coverageReport: CodeCoverageReport = {
        type: ReportType.CODE_COVERAGE,
        title: 'Code Coverage',
        currentValue: 78.5,
        status: 'PASSING',
        coverage: {
          line: 80.2,
          branch: 75.6,
          statement: 81.3,
          function: 90.0,
        },
      };

      expect(getReportSummary(owaspReport)).toBe('OWASP compliance: 85%, issues in 1 categories');
      expect(getReportSummary(coverageReport)).toBe('Code coverage: 78.5% (Line: 80.2%)');
    });
  });

  describe('MetricState Union', () => {
    it('should create different metric states with correct discriminants', () => {
      // Passing metric
      const passingMetric: PassingMetric = {
        shortcode: 'LCV',
        name: 'Line Coverage',
        unit: '%',
        positiveDirection: 'UPWARD',
        value: 85.2,
        status: MetricThresholdStatus.PASSING,
        threshold: 80.0,
        margin: 5.2,
      };

      // Failing metric
      const failingMetric: FailingMetric = {
        shortcode: 'DDP',
        name: 'Duplicate Code Percentage',
        unit: '%',
        positiveDirection: 'DOWNWARD',
        value: 12.5,
        status: MetricThresholdStatus.FAILING,
        threshold: 10.0,
        gap: 2.5,
        recommendations: [
          'Extract common code into shared functions',
          'Use inheritance or composition to reduce duplication',
        ],
      };

      expect(passingMetric.status).toBe(MetricThresholdStatus.PASSING);
      expect(failingMetric.status).toBe(MetricThresholdStatus.FAILING);

      // Type guard should correctly identify metric states
      expect(isMetricInState<PassingMetric>(passingMetric, MetricThresholdStatus.PASSING)).toBe(
        true
      );
      expect(isMetricInState<FailingMetric>(failingMetric, MetricThresholdStatus.FAILING)).toBe(
        true
      );
      expect(isMetricInState<FailingMetric>(passingMetric, MetricThresholdStatus.FAILING)).toBe(
        false
      );
    });

    it('should handle metric state specific properties', () => {
      // Function that returns different messages based on metric state
      const getMetricMessage = (metric: MetricState): string => {
        switch (metric.status) {
          case MetricThresholdStatus.PASSING:
            return `${metric.name} is passing at ${metric.value}${metric.unit} (exceeds threshold by ${(metric as PassingMetric).margin}${metric.unit})`;
          case MetricThresholdStatus.FAILING:
            return `${metric.name} is failing at ${metric.value}${metric.unit} (below threshold by ${(metric as FailingMetric).gap}${metric.unit})`;
          case MetricThresholdStatus.UNKNOWN:
            return `${metric.name} has no threshold set, current value is ${metric.value}${metric.unit}`;
          default:
            return `${metric.name} has unknown status: ${metric.status}, value: ${metric.value}${metric.unit}`;
        }
      };

      const passingMetric: PassingMetric = {
        shortcode: 'LCV',
        name: 'Line Coverage',
        unit: '%',
        positiveDirection: 'UPWARD',
        value: 85.2,
        status: MetricThresholdStatus.PASSING,
        threshold: 80.0,
        margin: 5.2,
      };

      const failingMetric: FailingMetric = {
        shortcode: 'DDP',
        name: 'Duplicate Code Percentage',
        unit: '%',
        positiveDirection: 'DOWNWARD',
        value: 12.5,
        status: MetricThresholdStatus.FAILING,
        threshold: 10.0,
        gap: 2.5,
      };

      expect(getMetricMessage(passingMetric)).toBe(
        'Line Coverage is passing at 85.2% (exceeds threshold by 5.2%)'
      );
      expect(getMetricMessage(failingMetric)).toBe(
        'Duplicate Code Percentage is failing at 12.5% (below threshold by 2.5%)'
      );
    });
  });
});
