const Container = require('../../../src/application/services/Container');

describe('Container - Dependency Injection', () => {
  let container;

  beforeEach(() => {
    container = new Container();
  });

  describe('constructor', () => {
    test('should initialize with empty Maps', () => {
      expect(container.services).toBeInstanceOf(Map);
      expect(container.factories).toBeInstanceOf(Map);
      expect(container.lifetimes).toBeInstanceOf(Map);
      expect(container.services.size).toBe(0);
      expect(container.factories.size).toBe(0);
      expect(container.lifetimes.size).toBe(0);
    });
  });

  describe('register', () => {
    test('should register a singleton service', () => {
      const factory = jest.fn(() => ({ id: 1, name: 'Test Service' }));
      
      container.register('TestService', factory, 'singleton');
      
      expect(container.factories.has('TestService')).toBe(true);
      expect(container.lifetimes.get('TestService')).toBe('singleton');
      expect(factory).toHaveBeenCalled(); // Singleton is created immediately
      expect(container.services.has('TestService')).toBe(true);
    });

    test('should register a transient service', () => {
      const factory = jest.fn(() => ({ id: 2, name: 'Transient Service' }));
      
      container.register('TransientService', factory, 'transient');
      
      expect(container.factories.has('TransientService')).toBe(true);
      expect(container.lifetimes.get('TransientService')).toBe('transient');
      expect(factory).not.toHaveBeenCalled(); // Transient is NOT created immediately
      expect(container.services.has('TransientService')).toBe(false);
    });

    test('should register a scoped service', () => {
      const factory = jest.fn(() => ({ id: 3, name: 'Scoped Service' }));
      
      container.register('ScopedService', factory, 'scoped');
      
      expect(container.factories.has('ScopedService')).toBe(true);
      expect(container.lifetimes.get('ScopedService')).toBe('scoped');
      expect(factory).not.toHaveBeenCalled(); // Scoped is NOT created immediately
    });

    test('should default to singleton if lifetime not specified', () => {
      const factory = () => ({ id: 4 });
      
      container.register('DefaultService', factory);
      
      expect(container.lifetimes.get('DefaultService')).toBe('singleton');
      expect(container.services.has('DefaultService')).toBe(true);
    });
  });

  describe('resolve - singleton', () => {
    test('should return the same instance for singleton', () => {
      container.register('SingletonService', () => ({ id: Math.random() }), 'singleton');
      
      const instance1 = container.resolve('SingletonService');
      const instance2 = container.resolve('SingletonService');
      
      expect(instance1).toBe(instance2);
      expect(instance1.id).toBe(instance2.id);
    });

    test('should return the pre-created singleton instance', () => {
      const mockInstance = { id: 123, name: 'Mock' };
      container.register('PreCreatedService', () => mockInstance, 'singleton');
      
      const resolved = container.resolve('PreCreatedService');
      
      expect(resolved).toBe(mockInstance);
    });
  });

  describe('resolve - transient', () => {
    test('should return different instances for transient', () => {
      container.register('TransientService', () => ({ id: Math.random() }), 'transient');
      
      const instance1 = container.resolve('TransientService');
      const instance2 = container.resolve('TransientService');
      
      expect(instance1).not.toBe(instance2);
      expect(instance1.id).not.toBe(instance2.id);
    });

    test('should call factory every time for transient', () => {
      const factory = jest.fn(() => ({ id: Math.random() }));
      container.register('TransientService', factory, 'transient');
      
      container.resolve('TransientService');
      container.resolve('TransientService');
      container.resolve('TransientService');
      
      expect(factory).toHaveBeenCalledTimes(3);
    });
  });

  describe('resolve - scoped', () => {
    test('should return same instance within the same scope', () => {
      container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
      
      const scope = container.createScope();
      const instance1 = scope.resolve('ScopedService');
      const instance2 = scope.resolve('ScopedService');
      
      expect(instance1).toBe(instance2);
      expect(instance1.id).toBe(instance2.id);
    });

    test('should return different instances for different scopes', () => {
      container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
      
      const scope1 = container.createScope();
      const scope2 = container.createScope();
      
      const instance1 = scope1.resolve('ScopedService');
      const instance2 = scope2.resolve('ScopedService');
      
      expect(instance1).not.toBe(instance2);
      expect(instance1.id).not.toBe(instance2.id);
    });

    test('should resolve scoped service with scope parameter', () => {
      const scopeMap = new Map();
      container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
      
      const instance1 = container.resolve('ScopedService', scopeMap);
      const instance2 = container.resolve('ScopedService', scopeMap);
      
      expect(instance1).toBe(instance2);
      expect(scopeMap.has('ScopedService')).toBe(true);
    });
  });

  describe('resolve - error handling', () => {
    test('should throw error for unregistered service', () => {
      expect(() => container.resolve('NonExistentService')).toThrow(
        "Service 'NonExistentService' is not registered"
      );
    });

    test('should provide clear error message', () => {
      try {
        container.resolve('MissingService');
        fail('Should have thrown error');
      } catch (error) {
        expect(error.message).toContain('MissingService');
        expect(error.message).toContain('not registered');
      }
    });
  });

  describe('createScope', () => {
    test('should create a scope with resolve method', () => {
      const scope = container.createScope();
      
      expect(scope).toHaveProperty('resolve');
      expect(typeof scope.resolve).toBe('function');
    });

    test('should create independent scopes', () => {
      container.register('ScopedService', () => ({ id: Math.random() }), 'scoped');
      
      const scope1 = container.createScope();
      const scope2 = container.createScope();
      
      const instance1 = scope1.resolve('ScopedService');
      const instance2 = scope2.resolve('ScopedService');
      
      expect(instance1.id).not.toBe(instance2.id);
    });
  });

  describe('real-world scenarios', () => {
    test('should handle database connection as singleton', () => {
      class DatabaseConnection {
        constructor() {
          this.id = Math.random();
          this.connected = true;
        }
      }

      container.register('Database', () => new DatabaseConnection(), 'singleton');

      const db1 = container.resolve('Database');
      const db2 = container.resolve('Database');

      expect(db1).toBe(db2);
      expect(db1.id).toBe(db2.id);
      expect(db1.connected).toBe(true);
    });

    test('should handle logger as transient', () => {
      class Logger {
        constructor() {
          this.id = Math.random();
        }
        log(msg) {
          return `[${this.id}] ${msg}`;
        }
      }

      container.register('Logger', () => new Logger(), 'transient');

      const logger1 = container.resolve('Logger');
      const logger2 = container.resolve('Logger');

      expect(logger1).not.toBe(logger2);
      expect(logger1.id).not.toBe(logger2.id);
    });

    test('should handle request context as scoped', () => {
      class RequestContext {
        constructor() {
          this.requestId = Math.random();
          this.user = null;
          this.timestamp = Date.now();
        }
      }

      container.register('RequestContext', () => new RequestContext(), 'scoped');

      // Request 1
      const request1Scope = container.createScope();
      const ctx1a = request1Scope.resolve('RequestContext');
      const ctx1b = request1Scope.resolve('RequestContext');
      expect(ctx1a).toBe(ctx1b);

      // Request 2
      const request2Scope = container.createScope();
      const ctx2 = request2Scope.resolve('RequestContext');
      expect(ctx1a).not.toBe(ctx2);
      expect(ctx1a.requestId).not.toBe(ctx2.requestId);
    });

    test('should handle dependency chain', () => {
      class Repository {
        constructor(db) {
          this.db = db;
        }
      }

      class Service {
        constructor(repo) {
          this.repo = repo;
        }
      }

      const mockDb = { id: 1, connected: true };
      container.register('Database', () => mockDb, 'singleton');
      container.register('Repository', () => {
        return new Repository(container.resolve('Database'));
      }, 'singleton');
      container.register('Service', () => {
        return new Service(container.resolve('Repository'));
      }, 'singleton');

      const service = container.resolve('Service');
      
      expect(service.repo).toBeDefined();
      expect(service.repo.db).toBe(mockDb);
    });
  });

  describe('mixed lifecycle scenarios', () => {
    test('should handle mixed singleton and transient', () => {
      container.register('Config', () => ({ port: 3000 }), 'singleton');
      container.register('Handler', () => ({ 
        id: Math.random(),
        config: container.resolve('Config')
      }), 'transient');

      const handler1 = container.resolve('Handler');
      const handler2 = container.resolve('Handler');

      // Different handlers
      expect(handler1).not.toBe(handler2);
      expect(handler1.id).not.toBe(handler2.id);

      // But same config
      expect(handler1.config).toBe(handler2.config);
    });

    test('should handle mixed singleton and scoped', () => {
      container.register('Database', () => ({ id: 'db' }), 'singleton');
      container.register('RequestData', () => ({ 
        id: Math.random()
      }), 'scoped');

      const scope1 = container.createScope();
      const scope2 = container.createScope();

      const data1a = scope1.resolve('RequestData');
      const data1b = scope1.resolve('RequestData');
      const data2 = scope2.resolve('RequestData');

      expect(data1a).toBe(data1b); // Same within scope
      expect(data1a).not.toBe(data2); // Different across scopes
    });
  });
});
