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

// Mock loader for testing
class MockLoader implements ConfigLoader {
  priority = 0;

  constructor(
    private data: Record<string, unknown>,
    public override priority: number = 0
  ) {}

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

// Test schema
const testSchema = z.object({
  app: z.object({
    name: z.string(),
    version: z.string(),
    port: z.number().positive(),
  }),
  database: z.object({
    host: z.string(),
    port: z.number(),
  }),
  environment: z.enum(['development', 'test', 'production']),
});

type TestConfig = z.infer<typeof testSchema>;

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

  beforeEach(() => {
    manager = new ConfigManager<TestConfig>({
      loaders: [
        new MockLoader({
          app: {
            name: 'test-app',
            version: '1.0.0',
            port: 3000,
          },
          database: {
            host: 'localhost',
            port: 5432,
          },
        }),
      ],
      environment: 'test',
    });
  });

  test('should initialize configuration', async () => {
    const config = await manager.initialize(testSchema);

    expect(config.app.name).toBe('test-app');
    expect(config.app.version).toBe('1.0.0');
    expect(config.environment).toBe('test');
  });

  test('should merge multiple loaders by priority', async () => {
    manager = new ConfigManager<TestConfig>({
      loaders: [
        new MockLoader({ app: { name: 'base', port: 3000 } }, 0),
        new MockLoader({ app: { name: 'override', version: '2.0.0' } }, 10),
        new MockLoader({ database: { host: 'prod-db' } }, 5),
      ],
      environment: 'test',
    });

    const config = await manager.initialize();

    expect(config.app.name).toBe('override');
    expect(config.app.version).toBe('2.0.0');
    expect(config.app.port).toBe(3000);
    expect(config.database.host).toBe('prod-db');
  });

  test('should validate configuration with schema', async () => {
    manager = new ConfigManager<TestConfig>({
      loaders: [
        new MockLoader({
          app: {
            name: 'test-app',
            version: '1.0.0',
            port: 'invalid', // Should be number
          },
        }),
      ],
    });

    await expect(manager.initialize(testSchema)).rejects.toThrow(ConfigValidationError);
  });

  test('should get configuration value by path', async () => {
    await manager.initialize(testSchema);

    expect(manager.getValue('app.name')).toBe('test-app');
    expect(manager.getValue<number>('database.port')).toBe(5432);
  });

  test('should check if configuration path exists', async () => {
    await manager.initialize(testSchema);

    expect(manager.has('app.name')).toBe(true);
    expect(manager.has('app.nonexistent')).toBe(false);
  });

  test('should update configuration at runtime', async () => {
    await manager.initialize(testSchema);

    manager.set({
      app: {
        name: 'updated-app',
      },
    });

    const config = manager.get();
    expect(config.app.name).toBe('updated-app');
    expect(config.app.version).toBe('1.0.0'); // Should preserve other values
  });

  test('should validate updates against schema', async () => {
    await manager.initialize(testSchema);

    expect(() => {
      manager.set({
        app: {
          port: 'invalid' as any,
        },
      });
    }).toThrow(ConfigValidationError);
  });

  test('should reset configuration', async () => {
    await manager.initialize(testSchema);
    manager.reset();

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

  test('should create typed getter', async () => {
    await manager.initialize(testSchema);

    const appSchema = z.object({
      app: z.object({
        name: z.string(),
        version: z.string(),
      }),
    });

    const getAppConfig = manager.createTypedGetter(appSchema);
    const appConfig = getAppConfig();

    expect(appConfig.app.name).toBe('test-app');
  });

  test('should detect environment correctly', () => {
    const originalEnv = process.env.NODE_ENV;

    process.env.NODE_ENV = 'production';
    const prodManager = new ConfigManager({ loaders: [] });
    expect(prodManager.getEnvironment()).toBe('production');

    process.env.NODE_ENV = 'test';
    const testManager = new ConfigManager({ loaders: [] });
    expect(testManager.getEnvironment()).toBe('test');

    process.env.NODE_ENV = originalEnv;
  });

  test('should handle deep merge correctly', async () => {
    manager = new ConfigManager({
      loaders: [
        new MockLoader(
          {
            app: {
              settings: {
                feature1: true,
                feature2: false,
                nested: {
                  value: 'base',
                },
              },
            },
          },
          0
        ),
        new MockLoader(
          {
            app: {
              settings: {
                feature2: true,
                feature3: true,
                nested: {
                  value: 'override',
                  extra: 'new',
                },
              },
            },
          },
          10
        ),
      ],
    });

    const config = await manager.initialize();

    expect(config.app.settings.feature1).toBe(true);
    expect(config.app.settings.feature2).toBe(true);
    expect(config.app.settings.feature3).toBe(true);
    expect(config.app.settings.nested.value).toBe('override');
    expect(config.app.settings.nested.extra).toBe('new');
  });
});
