import { beforeEach, describe, expect, it, mock } from 'bun:test';
import { OperationContext } from '../src/operation-context';
import type { OperationContextOptions } from '../src/operation-context';

describe('OperationContext', () => {
  const mockLogger = {
    info: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
    debug: mock(() => {}),
    trace: mock(() => {}),
    child: mock(() => mockLogger),
  };

  const mockContainer = {
    resolve: mock((name: string) => ({ name })),
    resolveAsync: mock(async (name: string) => ({ name })),
  };

  beforeEach(() => {
    // Reset mocks
    Object.keys(mockLogger).forEach(key => {
      if (typeof mockLogger[key as keyof typeof mockLogger] === 'function') {
        (mockLogger as any)[key] = mock(() => (key === 'child' ? mockLogger : undefined));
      }
    });
    mockContainer.resolve = mock((name: string) => ({ name }));
    mockContainer.resolveAsync = mock(async (name: string) => ({ name }));
  });

  describe('constructor', () => {
    it('should create context with required options', () => {
      const options: OperationContextOptions = {
        handlerName: 'test-handler',
        operationName: 'test-op',
      };

      const context = new OperationContext(options);

      expect(context).toBeDefined();
      expect(context.traceId).toBeDefined();
      expect(context.metadata).toEqual({});
      expect(context.logger).toBeDefined();
    });

    it('should create context with all options', () => {
      const options: OperationContextOptions = {
        handlerName: 'test-handler',
        operationName: 'test-op',
        parentLogger: mockLogger,
        container: mockContainer,
        metadata: { key: 'value' },
        traceId: 'custom-trace-id',
      };

      const context = new OperationContext(options);

      expect(context.traceId).toBe('custom-trace-id');
      expect(context.metadata).toEqual({ key: 'value' });
      expect(context.logger).toBe(mockLogger);
    });
  });

  describe('static create', () => {
    it('should create context using static method', () => {
      const context = OperationContext.create('handler', 'operation', {
        metadata: { foo: 'bar' },
      });

      expect(context).toBeDefined();
      expect(context.metadata).toEqual({ foo: 'bar' });
    });
  });

  describe('service resolution', () => {
    it('should resolve services from container', () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
        container: mockContainer,
      });

      const service = context.resolve('myService');
      expect(service).toEqual({ name: 'myService' });
      expect(mockContainer.resolve).toHaveBeenCalledWith('myService');
    });

    it('should resolve services asynchronously', async () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
        container: mockContainer,
      });

      const service = await context.resolveAsync('myService');
      expect(service).toEqual({ name: 'myService' });
      expect(mockContainer.resolveAsync).toHaveBeenCalledWith('myService');
    });

    it('should throw error when no container available', () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
      });

      expect(() => context.resolve('service')).toThrow('No service container available');
    });

    it('should throw error when no container available for async', async () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
      });

      await expect(context.resolveAsync('service')).rejects.toThrow(
        'No service container available'
      );
    });
  });

  describe('metadata', () => {
    it('should add metadata', () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
      });

      context.addMetadata('userId', '12345');
      context.addMetadata('correlationId', 'corr-456');

      expect(context.metadata.userId).toBe('12345');
      expect(context.metadata.correlationId).toBe('corr-456');
    });

    it('should preserve initial metadata', () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
        metadata: { initial: 'value' },
      });

      context.addMetadata('added', 'new-value');

      expect(context.metadata.initial).toBe('value');
      expect(context.metadata.added).toBe('new-value');
    });
  });

  describe('execution time', () => {
    it('should track execution time', async () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
      });

      // Wait a bit
      await new Promise(resolve => setTimeout(resolve, 50));

      const executionTime = context.getExecutionTime();
      expect(executionTime).toBeGreaterThan(40);
      expect(executionTime).toBeLessThan(100);
    });
  });

  describe('logging', () => {
    it('should log successful completion', () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
        parentLogger: mockLogger,
      });

      context.logCompletion(true);

      expect(mockLogger.info).toHaveBeenCalledWith(
        'Operation completed successfully',
        expect.objectContaining({
          executionTime: expect.any(Number),
          metadata: {},
        })
      );
    });

    it('should log failed completion with error', () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
        parentLogger: mockLogger,
      });

      const error = new Error('Test error');
      context.logCompletion(false, error);

      expect(mockLogger.error).toHaveBeenCalledWith(
        'Operation failed',
        expect.objectContaining({
          executionTime: expect.any(Number),
          error: 'Test error',
          stack: expect.any(String),
          metadata: {},
        })
      );
    });
  });

  describe('child context', () => {
    it('should create child context', () => {
      const parent = new OperationContext({
        handlerName: 'parent',
        operationName: 'parent-op',
        parentLogger: mockLogger,
        container: mockContainer,
        traceId: 'parent-trace',
        metadata: { parentKey: 'parentValue' },
      });

      const child = parent.createChild('child-op', { childKey: 'childValue' });

      expect(child.traceId).toBe('parent-trace'); // Inherits trace ID
      expect(child.metadata).toEqual({
        parentKey: 'parentValue',
        childKey: 'childValue',
      });
      expect(child.logger).toBe(mockLogger); // Inherits logger
    });

    it('should create child without additional metadata', () => {
      const parent = new OperationContext({
        handlerName: 'parent',
        operationName: 'parent-op',
        metadata: { key: 'value' },
      });

      const child = parent.createChild('child-op');

      expect(child.metadata).toEqual({ key: 'value' });
    });
  });

  describe('dispose', () => {
    it('should log completion on dispose', async () => {
      const context = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
        parentLogger: mockLogger,
      });

      await context.dispose();

      expect(mockLogger.info).toHaveBeenCalledWith(
        'Operation completed successfully',
        expect.any(Object)
      );
    });
  });

  describe('trace ID generation', () => {
    it('should generate unique trace IDs', () => {
      const context1 = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
      });

      const context2 = new OperationContext({
        handlerName: 'test',
        operationName: 'test-op',
      });

      expect(context1.traceId).not.toBe(context2.traceId);
      expect(context1.traceId).toMatch(/^\d+-[a-z0-9]+$/);
    });
  });
});
