import { beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import { z } from 'zod';
import { ConfigManager } from '../src/config-manager';
import { ConfigError, ConfigValidationError } from '../src/errors';
import type { ConfigLoader, Environment } from '../src/types';

// Mock the logger
mock.module('@stock-bot/logger', () => ({
  getLogger: () => ({
    info: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
    debug: mock(() => {}),
  }),
}));

// Mock loader class
class MockLoader implements ConfigLoader {
  constructor(
    private data: Record<string, unknown>,
    public priority: number = 0
  ) {}

  load(): Record<string, unknown> {
    return this.data;
  }
}

describe('ConfigManager', () => {
  let manager: ConfigManager<any>;

  beforeEach(() => {
    // Reset environment
    delete process.env.NODE_ENV;
  });

  describe('constructor', () => {
    it('should initialize with default loaders', () => {
      manager = new ConfigManager();
      expect(manager).toBeDefined();
      expect(manager.getEnvironment()).toBe('development');
    });

    it('should detect environment from NODE_ENV', () => {
      process.env.NODE_ENV = 'production';
      manager = new ConfigManager();
      expect(manager.getEnvironment()).toBe('production');
    });

    it('should handle various environment values', () => {
      const envMap: Record<string, Environment> = {
        production: 'production',
        prod: 'production',
        test: 'test',
        development: 'development',
        dev: 'development',
        unknown: 'development',
      };

      for (const [input, expected] of Object.entries(envMap)) {
        process.env.NODE_ENV = input;
        manager = new ConfigManager();
        expect(manager.getEnvironment()).toBe(expected);
      }
    });

    it('should use custom loaders when provided', () => {
      const customLoader = new MockLoader({ custom: 'data' });
      manager = new ConfigManager({
        loaders: [customLoader],
      });

      manager.initialize();
      expect(manager.get()).toEqual({ custom: 'data', environment: 'development' });
    });

    it('should use custom environment when provided', () => {
      manager = new ConfigManager({
        environment: 'test',
      });
      expect(manager.getEnvironment()).toBe('test');
    });
  });

  describe('initialize', () => {
    it('should load and merge configurations', () => {
      const loader1 = new MockLoader({ a: 1, b: { c: 2 } }, 1);
      const loader2 = new MockLoader({ b: { d: 3 }, e: 4 }, 2);

      manager = new ConfigManager({
        loaders: [loader1, loader2],
      });

      const config = manager.initialize();

      expect(config).toEqual({
        a: 1,
        b: { c: 2, d: 3 },
        e: 4,
        environment: 'development',
      });
    });

    it('should return cached config on subsequent calls', () => {
      const loader = new MockLoader({ test: 'data' });
      const loadSpy = spyOn(loader, 'load');

      manager = new ConfigManager({
        loaders: [loader],
      });

      const config1 = manager.initialize();
      const config2 = manager.initialize();

      expect(config1).toBe(config2);
      expect(loadSpy).toHaveBeenCalledTimes(1);
    });

    it('should validate config with schema', () => {
      const schema = z.object({
        name: z.string(),
        port: z.number(),
        environment: z.string(),
      });

      const loader = new MockLoader({
        name: 'test-app',
        port: 3000,
      });

      manager = new ConfigManager({
        loaders: [loader],
      });

      const config = manager.initialize(schema);

      expect(config).toEqual({
        name: 'test-app',
        port: 3000,
        environment: 'development',
      });
    });

    it('should throw validation error for invalid config', () => {
      const schema = z.object({
        name: z.string(),
        port: z.number(),
      });

      const loader = new MockLoader({
        name: 'test-app',
        port: 'invalid', // Should be number
      });

      manager = new ConfigManager({
        loaders: [loader],
      });

      expect(() => manager.initialize(schema)).toThrow(ConfigValidationError);
    });

    it('should handle empty loaders', () => {
      manager = new ConfigManager({
        loaders: [],
      });

      const config = manager.initialize();
      expect(config).toEqual({ environment: 'development' });
    });

    it('should ignore loaders that return empty config', () => {
      const loader1 = new MockLoader({});
      const loader2 = new MockLoader({ data: 'value' });

      manager = new ConfigManager({
        loaders: [loader1, loader2],
      });

      const config = manager.initialize();
      expect(config).toEqual({ data: 'value', environment: 'development' });
    });

    it('should respect loader priority order', () => {
      const loader1 = new MockLoader({ value: 'first' }, 1);
      const loader2 = new MockLoader({ value: 'second' }, 2);
      const loader3 = new MockLoader({ value: 'third' }, 0);

      manager = new ConfigManager({
        loaders: [loader1, loader2, loader3],
      });

      const config = manager.initialize();
      // Priority order: 0, 1, 2 (lowest to highest)
      // So 'second' should win
      expect(config.value).toBe('second');
    });

    it('should handle validation errors with detailed error info', () => {
      const schema = z.object({
        name: z.string(),
        port: z.number().min(1).max(65535),
        features: z.object({
          enabled: z.boolean(),
        }),
      });

      const loader = new MockLoader({
        name: 123, // Should be string
        port: 99999, // Out of range
        features: {
          enabled: 'yes', // Should be boolean
        },
      });

      manager = new ConfigManager({
        loaders: [loader],
      });

      try {
        manager.initialize(schema);
        expect(true).toBe(false); // Should not reach here
      } catch (error) {
        expect(error).toBeInstanceOf(ConfigValidationError);
        const validationError = error as ConfigValidationError;
        expect(validationError.errors).toBeDefined();
        expect(validationError.errors.length).toBeGreaterThan(0);
      }
    });
  });

  describe('get', () => {
    it('should return config after initialization', () => {
      const loader = new MockLoader({ test: 'data' });
      manager = new ConfigManager({ loaders: [loader] });

      manager.initialize();
      expect(manager.get()).toEqual({ test: 'data', environment: 'development' });
    });

    it('should throw error if not initialized', () => {
      manager = new ConfigManager();

      expect(() => manager.get()).toThrow(ConfigError);
      expect(() => manager.get()).toThrow('Configuration not initialized');
    });
  });

  describe('getValue', () => {
    beforeEach(() => {
      const loader = new MockLoader({
        database: {
          host: 'localhost',
          port: 5432,
          credentials: {
            username: 'admin',
            password: 'secret',
          },
        },
        cache: {
          enabled: true,
          ttl: 3600,
        },
      });

      manager = new ConfigManager({ loaders: [loader] });
      manager.initialize();
    });

    it('should get value by path', () => {
      expect(manager.getValue('database.host')).toBe('localhost');
      expect(manager.getValue('database.port')).toBe(5432);
      expect(manager.getValue('cache.enabled')).toBe(true);
    });

    it('should get nested values', () => {
      expect(manager.getValue('database.credentials.username')).toBe('admin');
      expect(manager.getValue('database.credentials.password')).toBe('secret');
    });

    it('should throw error for non-existent path', () => {
      expect(() => manager.getValue('nonexistent.path')).toThrow(ConfigError);
      expect(() => manager.getValue('nonexistent.path')).toThrow('Configuration key not found');
    });

    it('should handle top-level values', () => {
      expect(manager.getValue('database')).toEqual({
        host: 'localhost',
        port: 5432,
        credentials: {
          username: 'admin',
          password: 'secret',
        },
      });
    });
  });

  describe('has', () => {
    beforeEach(() => {
      const loader = new MockLoader({
        database: { host: 'localhost' },
        cache: { enabled: true },
      });

      manager = new ConfigManager({ loaders: [loader] });
      manager.initialize();
    });

    it('should return true for existing paths', () => {
      expect(manager.has('database')).toBe(true);
      expect(manager.has('database.host')).toBe(true);
      expect(manager.has('cache.enabled')).toBe(true);
    });

    it('should return false for non-existent paths', () => {
      expect(manager.has('nonexistent')).toBe(false);
      expect(manager.has('database.port')).toBe(false);
      expect(manager.has('cache.ttl')).toBe(false);
    });
  });

  describe('set', () => {
    beforeEach(() => {
      const loader = new MockLoader({
        app: { name: 'test', version: '1.0.0' },
        port: 3000,
      });

      manager = new ConfigManager({ loaders: [loader] });
      manager.initialize();
    });

    it('should update configuration values', () => {
      manager.set({ port: 4000 });
      expect(manager.get().port).toBe(4000);

      manager.set({ app: { version: '2.0.0' } });
      expect(manager.get().app.version).toBe('2.0.0');
      expect(manager.get().app.name).toBe('test'); // Unchanged
    });

    it('should validate updates when schema is present', () => {
      const schema = z.object({
        app: z.object({
          name: z.string(),
          version: z.string(),
        }),
        port: z.number().min(1000).max(9999),
        environment: z.string(),
      });

      manager = new ConfigManager({
        loaders: [new MockLoader({ app: { name: 'test', version: '1.0.0' }, port: 3000 })],
      });
      manager.initialize(schema);

      // Valid update
      manager.set({ port: 4000 });
      expect(manager.get().port).toBe(4000);

      // Invalid update
      expect(() => manager.set({ port: 99999 })).toThrow(ConfigValidationError);
    });

    it('should throw error if not initialized', () => {
      const newManager = new ConfigManager();
      expect(() => newManager.set({ test: 'value' })).toThrow(ConfigError);
    });
  });

  describe('reset', () => {
    it('should clear configuration', () => {
      const loader = new MockLoader({ test: 'data' });
      manager = new ConfigManager({ loaders: [loader] });

      manager.initialize();
      expect(manager.get()).toBeDefined();

      manager.reset();
      expect(() => manager.get()).toThrow(ConfigError);
    });
  });

  describe('validate', () => {
    it('should validate current config against schema', () => {
      const loader = new MockLoader({
        name: 'test-app',
        port: 3000,
      });

      manager = new ConfigManager({ loaders: [loader] });
      manager.initialize();

      const schema = z.object({
        name: z.string(),
        port: z.number(),
        environment: z.string(),
      });

      const validated = manager.validate(schema);
      expect(validated).toEqual({
        name: 'test-app',
        port: 3000,
        environment: 'development',
      });
    });

    it('should throw if validation fails', () => {
      const loader = new MockLoader({
        name: 'test-app',
        port: 'invalid',
      });

      manager = new ConfigManager({ loaders: [loader] });
      manager.initialize();

      const schema = z.object({
        name: z.string(),
        port: z.number(),
      });

      expect(() => manager.validate(schema)).toThrow();
    });
  });


  describe('deepMerge', () => {
    it('should handle circular references', () => {
      const obj1: any = { a: 1 };
      const obj2: any = { b: 2 };
      obj1.circular = obj1; // Create circular reference
      obj2.ref = obj1;

      const loader1 = new MockLoader(obj1);
      const loader2 = new MockLoader(obj2);

      manager = new ConfigManager({ loaders: [loader1, loader2] });

      // Should not throw on circular reference
      const config = manager.initialize();
      expect(config.a).toBe(1);
      expect(config.b).toBe(2);
    });

    it('should handle null and undefined values', () => {
      const loader1 = new MockLoader({ a: null, b: 'value' });
      const loader2 = new MockLoader({ a: 'overridden', c: undefined });

      manager = new ConfigManager({ loaders: [loader1, loader2] });
      const config = manager.initialize();

      expect(config.a).toBe('overridden');
      expect(config.b).toBe('value');
      expect(config.c).toBeUndefined();
    });

    it('should handle Date and RegExp objects', () => {
      const date = new Date('2024-01-01');
      const regex = /test/gi;

      const loader = new MockLoader({
        date: date,
        pattern: regex,
        nested: {
          date: date,
          pattern: regex,
        },
      });

      manager = new ConfigManager({ loaders: [loader] });
      const config = manager.initialize();

      expect(config.date).toBe(date);
      expect(config.pattern).toBe(regex);
      expect(config.nested.date).toBe(date);
      expect(config.nested.pattern).toBe(regex);
    });

    it('should handle arrays without merging', () => {
      const loader1 = new MockLoader({ items: [1, 2, 3] });
      const loader2 = new MockLoader({ items: [4, 5, 6] });

      manager = new ConfigManager({ loaders: [loader1, loader2] });
      const config = manager.initialize();

      // Arrays should be replaced, not merged
      expect(config.items).toEqual([4, 5, 6]);
    });
  });
});
