/**
 * 统一配置管理服务测试
 * 
 * 功能描述：
 * - 测试统一配置管理服务的核心功能
 * - 验证配置文件的加载、同步和备份机制
 * - 测试远程配置获取和本地配置管理
 * - 测试错误处理和容错机制
 * - 确保配置管理的可靠性和健壮性
 * 
 * 最近修改：
 * - 2026-01-12: 代码规范优化 - 创建缺失的测试文件，确保测试覆盖完整性 (修改者: moyin)
 * 
 * @author moyin
 * @version 1.0.0
 * @since 2026-01-12
 * @lastModified 2026-01-12
 */

import { Test, TestingModule } from '@nestjs/testing';
import { DynamicConfigManagerService } from './dynamic_config_manager.service';
import { ConfigManagerService } from './config_manager.service';
import * as fs from 'fs';
import * as path from 'path';
import axios from 'axios';

// Mock fs module
jest.mock('fs');
jest.mock('axios');

const mockedFs = fs as jest.Mocked<typeof fs>;
const mockedAxios = axios as jest.Mocked<typeof axios>;

describe('DynamicConfigManagerService', () => {
  let service: DynamicConfigManagerService;
  let configManagerService: jest.Mocked<ConfigManagerService>;

  const mockConfig = {
    version: '2.0.0',
    lastModified: '2026-01-12T00:00:00.000Z',
    description: '测试配置',
    source: 'local',
    maps: [
      {
        mapId: 'whale_port',
        mapName: '鲸之港',
        zulipStream: 'Whale Port',
        zulipStreamId: 5,
        description: '中心城区',
        isPublic: true,
        isWebPublic: false,
        interactionObjects: [
          {
            objectId: 'whale_port_general',
            objectName: 'General讨论区',
            zulipTopic: 'General',
            position: { x: 100, y: 100 },
            lastMessageId: 0
          }
        ]
      }
    ]
  };

  beforeEach(async () => {
    // 清除所有mock
    jest.clearAllMocks();

    // 设置环境变量 - 必须在创建服务实例之前设置
    process.env.ZULIP_SERVER_URL = 'https://test.zulip.com';
    process.env.ZULIP_BOT_EMAIL = 'bot@test.com';
    process.env.ZULIP_BOT_API_KEY = 'test-api-key';

    const mockConfigManager = {
      getStreamByMap: jest.fn(),
      getMapIdByStream: jest.fn(),
      getTopicByObject: jest.fn(),
      getZulipConfig: jest.fn(),
      hasMap: jest.fn(),
      hasStream: jest.fn(),
      getAllMapIds: jest.fn(),
      getAllStreams: jest.fn(),
      reloadConfig: jest.fn(),
      validateConfig: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        DynamicConfigManagerService,
        {
          provide: ConfigManagerService,
          useValue: mockConfigManager,
        },
      ],
    }).compile();

    service = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
    configManagerService = module.get(ConfigManagerService);

    // Mock fs methods
    mockedFs.existsSync.mockReturnValue(true);
    mockedFs.mkdirSync.mockImplementation();
    mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig));
    mockedFs.writeFileSync.mockImplementation();
    mockedFs.copyFileSync.mockImplementation();
    mockedFs.readdirSync.mockReturnValue([]);
    mockedFs.statSync.mockReturnValue({ mtime: new Date() } as any);
    mockedFs.unlinkSync.mockImplementation();
  });

  afterEach(() => {
    // 清理环境变量
    delete process.env.ZULIP_SERVER_URL;
    delete process.env.ZULIP_BOT_EMAIL;
    delete process.env.ZULIP_BOT_API_KEY;
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('initialization', () => {
    it('should initialize with Zulip credentials', () => {
      expect(service).toBeDefined();
      // 验证构造函数正确处理了环境变量
    });

    it('should initialize without Zulip credentials', async () => {
      // 临时清除环境变量
      const originalUrl = process.env.ZULIP_SERVER_URL;
      const originalEmail = process.env.ZULIP_BOT_EMAIL;
      const originalKey = process.env.ZULIP_BOT_API_KEY;
      
      delete process.env.ZULIP_SERVER_URL;
      delete process.env.ZULIP_BOT_EMAIL;
      delete process.env.ZULIP_BOT_API_KEY;

      try {
        const mockConfigManager = {
          getStreamByMap: jest.fn(),
          getMapIdByStream: jest.fn(),
          getTopicByObject: jest.fn(),
          getZulipConfig: jest.fn(),
          hasMap: jest.fn(),
          hasStream: jest.fn(),
          getAllMapIds: jest.fn(),
          getAllStreams: jest.fn(),
          reloadConfig: jest.fn(),
          validateConfig: jest.fn(),
        };

        const module = await Test.createTestingModule({
          providers: [
            DynamicConfigManagerService,
            {
              provide: ConfigManagerService,
              useValue: mockConfigManager,
            },
          ],
        }).compile();

        const serviceWithoutCredentials = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);
        expect(serviceWithoutCredentials).toBeDefined();
      } catch (error) {
        // 预期会抛出错误，因为缺少必要的Zulip凭据
        expect((error as Error).message).toContain('Zulip凭据未配置');
      } finally {
        // 恢复环境变量
        if (originalUrl) process.env.ZULIP_SERVER_URL = originalUrl;
        if (originalEmail) process.env.ZULIP_BOT_EMAIL = originalEmail;
        if (originalKey) process.env.ZULIP_BOT_API_KEY = originalKey;
      }
    });
  });

  describe('testZulipConnection', () => {
    it('should return true for successful connection', async () => {
      mockedAxios.get.mockResolvedValue({ status: 200, data: {} });

      const result = await service.testZulipConnection();

      expect(result).toBe(true);
      expect(mockedAxios.get).toHaveBeenCalledWith(
        'https://test.zulip.com/api/v1/users/me',
        expect.objectContaining({
          headers: expect.objectContaining({
            'Authorization': expect.stringContaining('Basic'),
            'Content-Type': 'application/json'
          }),
          timeout: 10000
        })
      );
    });

    it('should return false for failed connection', async () => {
      mockedAxios.get.mockRejectedValue(new Error('Connection failed'));

      const result = await service.testZulipConnection();

      expect(result).toBe(false);
    });

    it('should return false when no credentials', async () => {
      // 临时保存原始值
      const originalUrl = process.env.ZULIP_SERVER_URL;
      
      delete process.env.ZULIP_SERVER_URL;

      const result = await service.testZulipConnection();

      expect(result).toBe(false);
      
      // 恢复原始值
      if (originalUrl) process.env.ZULIP_SERVER_URL = originalUrl;
    });
  });

  describe('getZulipStreams', () => {
    const mockStreams = [
      {
        stream_id: 5,
        name: 'Whale Port',
        description: 'Main port area',
        invite_only: false,
        is_web_public: false,
        stream_post_policy: 1,
        message_retention_days: null,
        history_public_to_subscribers: true,
        first_message_id: null,
        is_announcement_only: false
      }
    ];

    it('should fetch streams successfully', async () => {
      mockedAxios.get.mockResolvedValue({
        status: 200,
        data: { streams: mockStreams }
      });

      const result = await service.getZulipStreams();

      expect(result).toEqual(mockStreams);
      expect(mockedAxios.get).toHaveBeenCalledWith(
        'https://test.zulip.com/api/v1/streams',
        expect.objectContaining({
          params: expect.objectContaining({
            include_public: true,
            include_subscribed: true,
            include_all_active: true,
            include_default: true
          })
        })
      );
    });

    it('should throw error when API fails', async () => {
      mockedAxios.get.mockRejectedValue(new Error('API Error'));

      await expect(service.getZulipStreams()).rejects.toThrow('API Error');
    });

    it('should throw error when no credentials', async () => {
      // 创建一个没有凭据的新服务实例
      const originalUrl = process.env.ZULIP_SERVER_URL;
      const originalEmail = process.env.ZULIP_BOT_EMAIL;
      const originalKey = process.env.ZULIP_BOT_API_KEY;
      
      delete process.env.ZULIP_SERVER_URL;
      delete process.env.ZULIP_BOT_EMAIL;
      delete process.env.ZULIP_BOT_API_KEY;

      const mockConfigManager = {
        getStreamByMap: jest.fn(),
        getMapIdByStream: jest.fn(),
        getTopicByObject: jest.fn(),
        getZulipConfig: jest.fn(),
        hasMap: jest.fn(),
        hasStream: jest.fn(),
        getAllMapIds: jest.fn(),
        getAllStreams: jest.fn(),
        reloadConfig: jest.fn(),
        validateConfig: jest.fn(),
      };

      const module = await Test.createTestingModule({
        providers: [
          DynamicConfigManagerService,
          {
            provide: ConfigManagerService,
            useValue: mockConfigManager,
          },
        ],
      }).compile();

      const serviceWithoutCredentials = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);

      await expect(serviceWithoutCredentials.getZulipStreams()).rejects.toThrow('缺少Zulip配置信息');

      // 恢复环境变量
      if (originalUrl) process.env.ZULIP_SERVER_URL = originalUrl;
      if (originalEmail) process.env.ZULIP_BOT_EMAIL = originalEmail;
      if (originalKey) process.env.ZULIP_BOT_API_KEY = originalKey;
    });
  });

  describe('getZulipTopics', () => {
    const mockTopics = [
      { name: 'General', max_id: 123 },
      { name: 'Random', max_id: 456 }
    ];

    it('should fetch topics successfully', async () => {
      mockedAxios.get.mockResolvedValue({
        status: 200,
        data: { topics: mockTopics }
      });

      const result = await service.getZulipTopics(5);

      expect(result).toEqual(mockTopics);
      expect(mockedAxios.get).toHaveBeenCalledWith(
        'https://test.zulip.com/api/v1/users/me/5/topics',
        expect.objectContaining({
          timeout: 10000
        })
      );
    });

    it('should return empty array on error', async () => {
      mockedAxios.get.mockRejectedValue(new Error('API Error'));

      const result = await service.getZulipTopics(5);

      expect(result).toEqual([]);
    });

    it('should throw error when no credentials', async () => {
      // 创建一个没有凭据的新服务实例
      const originalUrl = process.env.ZULIP_SERVER_URL;
      const originalEmail = process.env.ZULIP_BOT_EMAIL;
      const originalKey = process.env.ZULIP_BOT_API_KEY;
      
      delete process.env.ZULIP_SERVER_URL;
      delete process.env.ZULIP_BOT_EMAIL;
      delete process.env.ZULIP_BOT_API_KEY;

      const mockConfigManager = {
        getStreamByMap: jest.fn(),
        getMapIdByStream: jest.fn(),
        getTopicByObject: jest.fn(),
        getZulipConfig: jest.fn(),
        hasMap: jest.fn(),
        hasStream: jest.fn(),
        getAllMapIds: jest.fn(),
        getAllStreams: jest.fn(),
        reloadConfig: jest.fn(),
        validateConfig: jest.fn(),
      };

      const module = await Test.createTestingModule({
        providers: [
          DynamicConfigManagerService,
          {
            provide: ConfigManagerService,
            useValue: mockConfigManager,
          },
        ],
      }).compile();

      const serviceWithoutCredentials = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);

      await expect(serviceWithoutCredentials.getZulipTopics(5)).rejects.toThrow('缺少Zulip配置信息');

      // 恢复环境变量
      if (originalUrl) process.env.ZULIP_SERVER_URL = originalUrl;
      if (originalEmail) process.env.ZULIP_BOT_EMAIL = originalEmail;
      if (originalKey) process.env.ZULIP_BOT_API_KEY = originalKey;
    });
  });

  describe('getConfig', () => {
    it('should return cached config when available', async () => {
      // 设置缓存
      (service as any).configCache = mockConfig;

      const result = await service.getConfig();

      expect(result).toEqual(mockConfig);
    });

    it('should load local config when cache is empty', async () => {
      mockedFs.existsSync.mockReturnValue(true);
      mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig));

      const result = await service.getConfig();

      expect(result).toEqual(mockConfig);
    });

    it('should create default config when no local config exists', async () => {
      // 创建一个新的服务实例来测试默认配置创建
      const mockConfigManager = {
        getStreamByMap: jest.fn(),
        getMapIdByStream: jest.fn(),
        getTopicByObject: jest.fn(),
        getZulipConfig: jest.fn(),
        hasMap: jest.fn(),
        hasStream: jest.fn(),
        getAllMapIds: jest.fn(),
        getAllStreams: jest.fn(),
        reloadConfig: jest.fn(),
        validateConfig: jest.fn(),
      };

      const module = await Test.createTestingModule({
        providers: [
          DynamicConfigManagerService,
          {
            provide: ConfigManagerService,
            useValue: mockConfigManager,
          },
        ],
      }).compile();

      const testService = module.get<DynamicConfigManagerService>(DynamicConfigManagerService);

      const defaultConfig = {
        version: '2.0.0',
        lastModified: new Date().toISOString(),
        description: '统一配置管理 - 默认配置',
        source: 'default',
        maps: [
          {
            mapId: 'whale_port',
            mapName: '鲸之港',
            zulipStream: 'Whale Port',
            zulipStreamId: 5,
            description: '中心城区，交通枢纽与主要聚会点',
            isPublic: true,
            isWebPublic: false,
            interactionObjects: [
              {
                objectId: 'whale_port_general',
                objectName: 'General讨论区',
                zulipTopic: 'General',
                position: { x: 100, y: 100 },
                lastMessageId: 0
              }
            ]
          }
        ]
      };

      // Mock fs behavior: first call returns false (no config), subsequent calls return true (config exists)
      let callCount = 0;
      mockedFs.existsSync.mockImplementation(() => {
        callCount++;
        return callCount > 1; // First call false, subsequent calls true
      });
      
      // Mock readFileSync to return the default config
      mockedFs.readFileSync.mockReturnValue(JSON.stringify(defaultConfig));
      mockedFs.writeFileSync.mockImplementation(() => {
        // Simulate file creation
      });

      const result = await testService.getConfig();

      expect(mockedFs.writeFileSync).toHaveBeenCalled();
      expect(result).toBeDefined();
      expect(result.maps).toBeDefined();
      expect(result.maps).toHaveLength(1);
    });
  });

  describe('syncConfig', () => {
    it('should sync config successfully', async () => {
      const mockStreams = [
        {
          stream_id: 5,
          name: 'Whale Port',
          description: 'Main port',
          invite_only: false,
          is_web_public: false
        }
      ];

      mockedAxios.get
        .mockResolvedValueOnce({ status: 200, data: {} }) // connection test
        .mockResolvedValueOnce({ status: 200, data: { streams: mockStreams } }) // get streams
        .mockResolvedValueOnce({ status: 200, data: { topics: [] } }); // get topics

      const result = await service.syncConfig();

      expect(result.success).toBe(true);
      expect(result.source).toBe('remote');
      expect(result.mapCount).toBeGreaterThan(0);
    });

    it('should handle sync failure gracefully', async () => {
      mockedAxios.get.mockRejectedValue(new Error('Connection failed'));

      const result = await service.syncConfig();

      expect(result.success).toBe(false);
      expect(result.error).toBe('无法连接到Zulip服务器');
    });
  });

  describe('getStreamByMap', () => {
    it('should return stream name for valid map ID', async () => {
      (service as any).configCache = mockConfig;

      const result = await service.getStreamByMap('whale_port');

      expect(result).toBe('Whale Port');
    });

    it('should return null for invalid map ID', async () => {
      (service as any).configCache = mockConfig;

      const result = await service.getStreamByMap('invalid_map');

      expect(result).toBeNull();
    });

    it('should handle errors gracefully', async () => {
      (service as any).configCache = null;
      mockedFs.existsSync.mockReturnValue(false);

      const result = await service.getStreamByMap('whale_port');

      expect(result).toBeNull();
    });
  });

  describe('getMapIdByStream', () => {
    it('should return map ID for valid stream name', async () => {
      (service as any).configCache = mockConfig;

      const result = await service.getMapIdByStream('Whale Port');

      expect(result).toBe('whale_port');
    });

    it('should return null for invalid stream name', async () => {
      (service as any).configCache = mockConfig;

      const result = await service.getMapIdByStream('Invalid Stream');

      expect(result).toBeNull();
    });
  });

  describe('getAllMapConfigs', () => {
    it('should return all map configurations', async () => {
      (service as any).configCache = mockConfig;

      const result = await service.getAllMapConfigs();

      expect(result).toEqual(mockConfig.maps);
      expect(result).toHaveLength(1);
    });

    it('should return empty array on error', async () => {
      (service as any).configCache = null;
      mockedFs.existsSync.mockReturnValue(false);

      const result = await service.getAllMapConfigs();

      expect(result).toEqual([]);
    });
  });

  describe('getConfigStatus', () => {
    it('should return config status information', () => {
      (service as any).configCache = mockConfig;
      (service as any).lastSyncTime = new Date();

      const result = service.getConfigStatus();

      expect(result).toMatchObject({
        hasRemoteCredentials: true,
        hasLocalConfig: true,
        configSource: 'local',
        configVersion: '2.0.0',
        mapCount: 1,
        objectCount: 1
      });
    });
  });

  describe('getBackupFiles', () => {
    it('should return list of backup files', () => {
      const mockFiles = ['map-config-backup-2026-01-12T10-00-00-000Z.json'];
      mockedFs.existsSync.mockReturnValue(true);
      mockedFs.readdirSync.mockReturnValue(mockFiles as any);
      mockedFs.statSync.mockReturnValue({
        size: 1024,
        mtime: new Date('2026-01-12T10:00:00.000Z')
      } as any);

      const result = service.getBackupFiles();

      expect(result).toHaveLength(1);
      expect(result[0]).toMatchObject({
        name: mockFiles[0],
        size: 1024
      });
    });

    it('should return empty array when backup directory does not exist', () => {
      mockedFs.existsSync.mockReturnValue(false);

      const result = service.getBackupFiles();

      expect(result).toEqual([]);
    });

    it('should handle errors gracefully', () => {
      mockedFs.existsSync.mockReturnValue(true);
      mockedFs.readdirSync.mockImplementation(() => {
        throw new Error('Read error');
      });

      const result = service.getBackupFiles();

      expect(result).toEqual([]);
    });
  });

  describe('restoreFromBackup', () => {
    it('should restore config from backup successfully', async () => {
      const backupFileName = 'map-config-backup-2026-01-12T10-00-00-000Z.json';
      mockedFs.existsSync.mockReturnValue(true);

      const result = await service.restoreFromBackup(backupFileName);

      expect(result).toBe(true);
      expect(mockedFs.copyFileSync).toHaveBeenCalledTimes(2); // backup current + restore
    });

    it('should return false when backup file does not exist', async () => {
      const backupFileName = 'non-existent-backup.json';
      mockedFs.existsSync.mockReturnValue(false);

      const result = await service.restoreFromBackup(backupFileName);

      expect(result).toBe(false);
    });

    it('should handle restore errors gracefully', async () => {
      const backupFileName = 'map-config-backup-2026-01-12T10-00-00-000Z.json';
      mockedFs.existsSync.mockReturnValue(true);
      mockedFs.copyFileSync.mockImplementation(() => {
        throw new Error('Copy error');
      });

      const result = await service.restoreFromBackup(backupFileName);

      expect(result).toBe(false);
    });
  });

  describe('cleanup', () => {
    it('should cleanup resources on module destroy', () => {
      const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
      (service as any).syncTimer = setInterval(() => {}, 1000);

      service.onModuleDestroy();

      expect(clearIntervalSpy).toHaveBeenCalled();
    });
  });
});