import { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest';

interface FiveTuple {
  tenantId: string;
  shopId: string;
  taskId: string;
  traceId: string;
  businessType: 'TOC' | 'TOB';
}

interface TraceSpan {
  tuple: FiveTuple;
  operation: string;
  timestamp: number;
  duration?: number;
  parentSpanId?: string;
  spanId: string;
  metadata?: Record<string, any>;
}

interface TraceValidationResult {
  isValid: boolean;
  missingFields: string[];
  errors: string[];
}

class TracingService {
  private spans: Map<string, TraceSpan[]> = new Map();
  private currentSpan: TraceSpan | null = null;

  startSpan(tuple: FiveTuple, operation: string, metadata?: Record<string, any>): TraceSpan {
    const span: TraceSpan = {
      tuple,
      operation,
      timestamp: Date.now(),
      spanId: `span-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
      metadata,
    };

    if (this.currentSpan) {
      span.parentSpanId = this.currentSpan.spanId;
    }

    const traceSpans = this.spans.get(tuple.traceId) || [];
    traceSpans.push(span);
    this.spans.set(tuple.traceId, traceSpans);

    this.currentSpan = span;
    return span;
  }

  endSpan(span: TraceSpan): void {
    span.duration = Date.now() - span.timestamp;
  }

  getSpans(traceId: string): TraceSpan[] {
    return this.spans.get(traceId) || [];
  }

  getAllSpans(): Map<string, TraceSpan[]> {
    return this.spans;
  }

  clearSpans(): void {
    this.spans.clear();
    this.currentSpan = null;
  }

  validateTuple(tuple: Partial<FiveTuple>): TraceValidationResult {
    const requiredFields: (keyof FiveTuple)[] = ['tenantId', 'shopId', 'taskId', 'traceId', 'businessType'];
    const missingFields: string[] = [];
    const errors: string[] = [];

    for (const field of requiredFields) {
      if (!tuple[field]) {
        missingFields.push(field);
      }
    }

    if (tuple.businessType && !['TOC', 'TOB'].includes(tuple.businessType)) {
      errors.push(`Invalid businessType: ${tuple.businessType}. Must be 'TOC' or 'TOB'`);
    }

    if (tuple.tenantId && !tuple.tenantId.startsWith('tenant-')) {
      errors.push('tenantId should start with "tenant-"');
    }

    if (tuple.shopId && !tuple.shopId.startsWith('shop-')) {
      errors.push('shopId should start with "shop-"');
    }

    if (tuple.traceId && !tuple.traceId.startsWith('trace-')) {
      errors.push('traceId should start with "trace-"');
    }

    return {
      isValid: missingFields.length === 0 && errors.length === 0,
      missingFields,
      errors,
    };
  }

  validateTraceContinuity(traceId: string): { isValid: boolean; gaps: string[] } {
    const spans = this.getSpans(traceId);
    const gaps: string[] = [];

    if (spans.length === 0) {
      return { isValid: false, gaps: ['No spans found for trace'] };
    }

    const tupleFields = ['tenantId', 'shopId', 'taskId', 'businessType'] as const;
    const firstSpan = spans[0];

    for (const field of tupleFields) {
      const values = new Set(spans.map((s) => s.tuple[field]));
      if (values.size > 1) {
        gaps.push(`Inconsistent ${field} across spans: ${Array.from(values).join(', ')}`);
      }
    }

    const sortedSpans = [...spans].sort((a, b) => a.timestamp - b.timestamp);
    for (let i = 1; i < sortedSpans.length; i++) {
      if (sortedSpans[i].parentSpanId && sortedSpans[i].parentSpanId !== sortedSpans[i - 1].spanId) {
        gaps.push(`Broken span chain at index ${i}`);
      }
    }

    return {
      isValid: gaps.length === 0,
      gaps,
    };
  }

  generateTraceReport(traceId: string): {
    traceId: string;
    totalSpans: number;
    totalDuration: number;
    operations: string[];
    tuple: FiveTuple | null;
  } {
    const spans = this.getSpans(traceId);
    if (spans.length === 0) {
      return {
        traceId,
        totalSpans: 0,
        totalDuration: 0,
        operations: [],
        tuple: null,
      };
    }

    const totalDuration = spans.reduce((sum, span) => sum + (span.duration || 0), 0);
    const operations = spans.map((s) => s.operation);

    return {
      traceId,
      totalSpans: spans.length,
      totalDuration,
      operations,
      tuple: spans[0].tuple,
    };
  }
}

describe('Five-Tuple Tracing Tests', () => {
  let tracingService: TracingService;

  beforeAll(() => {
    tracingService = new TracingService();
  });

  beforeEach(() => {
    tracingService.clearSpans();
  });

  afterAll(() => {
    vi.clearAllMocks();
  });

  describe('Tuple Validation Tests', () => {
    it('should validate complete five-tuple', () => {
      const tuple: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-001',
        traceId: 'trace-001',
        businessType: 'TOC',
      };

      const result = tracingService.validateTuple(tuple);

      expect(result.isValid).toBe(true);
      expect(result.missingFields).toHaveLength(0);
      expect(result.errors).toHaveLength(0);
    });

    it('should detect missing fields', () => {
      const incompleteTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
      };

      const result = tracingService.validateTuple(incompleteTuple);

      expect(result.isValid).toBe(false);
      expect(result.missingFields).toContain('taskId');
      expect(result.missingFields).toContain('traceId');
      expect(result.missingFields).toContain('businessType');
    });

    it('should validate businessType values', () => {
      const invalidTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-001',
        traceId: 'trace-001',
        businessType: 'INVALID',
      };

      const result = tracingService.validateTuple(invalidTuple);

      expect(result.isValid).toBe(false);
      expect(result.errors).toContainEqual(expect.stringContaining('Invalid businessType'));
    });

    it('should validate ID prefixes', () => {
      const invalidPrefixTuple = {
        tenantId: 'invalid-tenant',
        shopId: 'invalid-shop',
        taskId: 'task-001',
        traceId: 'invalid-trace',
        businessType: 'TOC',
      };

      const result = tracingService.validateTuple(invalidPrefixTuple);

      expect(result.isValid).toBe(false);
      expect(result.errors.length).toBeGreaterThan(0);
    });
  });

  describe('Span Management Tests', () => {
    it('should create and track spans', () => {
      const tuple: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-001',
        traceId: 'trace-001',
        businessType: 'TOC',
      };

      const span = tracingService.startSpan(tuple, 'CREATE_ORDER');

      expect(span.spanId).toBeDefined();
      expect(span.operation).toBe('CREATE_ORDER');
      expect(span.tuple).toEqual(tuple);

      const spans = tracingService.getSpans('trace-001');
      expect(spans).toHaveLength(1);
    });

    it('should create parent-child span relationships', () => {
      const tuple: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-001',
        traceId: 'trace-002',
        businessType: 'TOC',
      };

      const parentSpan = tracingService.startSpan(tuple, 'CREATE_ORDER');
      const childSpan = tracingService.startSpan(tuple, 'ALLOCATE_STOCK');

      expect(childSpan.parentSpanId).toBe(parentSpan.spanId);
    });

    it('should track span duration', () => {
      const tuple: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-001',
        traceId: 'trace-003',
        businessType: 'TOC',
      };

      const span = tracingService.startSpan(tuple, 'PROCESS_PAYMENT');
      await new Promise((resolve) => setTimeout(resolve, 10));
      tracingService.endSpan(span);

      expect(span.duration).toBeDefined();
      expect(span.duration).toBeGreaterThanOrEqual(10);
    });
  });

  describe('Trace Continuity Tests', () => {
    it('should validate trace continuity', () => {
      const tuple: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-001',
        traceId: 'trace-004',
        businessType: 'TOC',
      };

      tracingService.startSpan(tuple, 'CREATE_ORDER');
      tracingService.startSpan(tuple, 'CONFIRM_ORDER');
      tracingService.startSpan(tuple, 'SHIP_ORDER');

      const result = tracingService.validateTraceContinuity('trace-004');

      expect(result.isValid).toBe(true);
      expect(result.gaps).toHaveLength(0);
    });

    it('should detect inconsistent tuple across spans', () => {
      const tuple1: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-001',
        traceId: 'trace-005',
        businessType: 'TOC',
      };

      const tuple2: FiveTuple = {
        tenantId: 'tenant-002',
        shopId: 'shop-001',
        taskId: 'task-001',
        traceId: 'trace-005',
        businessType: 'TOC',
      };

      tracingService.startSpan(tuple1, 'CREATE_ORDER');
      tracingService.startSpan(tuple2, 'CONFIRM_ORDER');

      const result = tracingService.validateTraceContinuity('trace-005');

      expect(result.isValid).toBe(false);
      expect(result.gaps).toContainEqual(expect.stringContaining('Inconsistent tenantId'));
    });

    it('should detect missing spans', () => {
      const result = tracingService.validateTraceContinuity('nonexistent-trace');

      expect(result.isValid).toBe(false);
      expect(result.gaps).toContain('No spans found for trace');
    });
  });

  describe('Cross-Module Tracing Tests', () => {
    it('should trace across multiple modules', () => {
      const tuple: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-cross-module',
        traceId: 'trace-cross-001',
        businessType: 'TOC',
      };

      tracingService.startSpan(tuple, 'PRODUCT_SERVICE:GET_PRODUCT');
      tracingService.startSpan(tuple, 'INVENTORY_SERVICE:CHECK_STOCK');
      tracingService.startSpan(tuple, 'ORDER_SERVICE:CREATE_ORDER');
      tracingService.startSpan(tuple, 'PAYMENT_SERVICE:PROCESS_PAYMENT');
      tracingService.startSpan(tuple, 'SHIPPING_SERVICE:CREATE_SHIPMENT');

      const spans = tracingService.getSpans('trace-cross-001');

      expect(spans).toHaveLength(5);
      expect(spans.map((s) => s.operation)).toContain('PRODUCT_SERVICE:GET_PRODUCT');
      expect(spans.map((s) => s.operation)).toContain('PAYMENT_SERVICE:PROCESS_PAYMENT');
    });

    it('should maintain tuple consistency across modules', () => {
      const tuple: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-consistency',
        traceId: 'trace-consistency-001',
        businessType: 'TOB',
      };

      const modules = ['PRODUCT', 'INVENTORY', 'ORDER', 'PAYMENT', 'SHIPPING'];
      modules.forEach((module) => {
        tracingService.startSpan(tuple, `${module}_SERVICE:OPERATION`);
      });

      const spans = tracingService.getSpans('trace-consistency-001');

      spans.forEach((span) => {
        expect(span.tuple.tenantId).toBe('tenant-001');
        expect(span.tuple.shopId).toBe('shop-001');
        expect(span.tuple.taskId).toBe('task-consistency');
        expect(span.tuple.businessType).toBe('TOB');
      });
    });
  });

  describe('Trace Report Tests', () => {
    it('should generate comprehensive trace report', () => {
      const tuple: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-report',
        traceId: 'trace-report-001',
        businessType: 'TOC',
      };

      const span1 = tracingService.startSpan(tuple, 'CREATE_ORDER');
      tracingService.endSpan(span1);

      const span2 = tracingService.startSpan(tuple, 'SHIP_ORDER');
      tracingService.endSpan(span2);

      const report = tracingService.generateTraceReport('trace-report-001');

      expect(report.traceId).toBe('trace-report-001');
      expect(report.totalSpans).toBe(2);
      expect(report.totalDuration).toBeGreaterThan(0);
      expect(report.operations).toContain('CREATE_ORDER');
      expect(report.operations).toContain('SHIP_ORDER');
      expect(report.tuple).toEqual(tuple);
    });

    it('should handle empty trace report', () => {
      const report = tracingService.generateTraceReport('nonexistent-trace');

      expect(report.totalSpans).toBe(0);
      expect(report.totalDuration).toBe(0);
      expect(report.operations).toHaveLength(0);
      expect(report.tuple).toBeNull();
    });
  });

  describe('Concurrent Tracing Tests', () => {
    it('should handle multiple concurrent traces', async () => {
      const traces = ['trace-concurrent-1', 'trace-concurrent-2', 'trace-concurrent-3'];

      const promises = traces.map((traceId) => {
        const tuple: FiveTuple = {
          tenantId: 'tenant-001',
          shopId: 'shop-001',
          taskId: `task-${traceId}`,
          traceId,
          businessType: 'TOC',
        };

        return Promise.resolve().then(() => {
          tracingService.startSpan(tuple, 'OPERATION_1');
          tracingService.startSpan(tuple, 'OPERATION_2');
          return traceId;
        });
      });

      const completedTraces = await Promise.all(promises);

      completedTraces.forEach((traceId) => {
        const spans = tracingService.getSpans(traceId);
        expect(spans).toHaveLength(2);
      });
    });
  });

  describe('Metadata Tests', () => {
    it('should store and retrieve span metadata', () => {
      const tuple: FiveTuple = {
        tenantId: 'tenant-001',
        shopId: 'shop-001',
        taskId: 'task-metadata',
        traceId: 'trace-metadata-001',
        businessType: 'TOC',
      };

      const metadata = {
        userId: 'user-001',
        ipAddress: '192.168.1.1',
        userAgent: 'Mozilla/5.0',
      };

      const span = tracingService.startSpan(tuple, 'API_REQUEST', metadata);

      expect(span.metadata).toEqual(metadata);
      expect(span.metadata?.userId).toBe('user-001');
    });
  });
});

export { TracingService, FiveTuple, TraceSpan };
