import { TRPCError } from '@trpc/server';
import dayjs from 'dayjs';
import { eq } from 'drizzle-orm/expressions';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { INBOX_SESSION_ID } from '@/const/session';
import { getTestDBInstance } from '@/database/core/dbForTest';
import { LobeChatDatabase } from '@/database/type';
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
import { UserGuide, UserPreference } from '@/types/user';

import { SessionModel } from '../../../models/session';
import { UserModel, UserNotFoundError } from '../../../models/user';
import { UserSettingsItem, userSettings, users } from '../../../schemas';

let serverDB = await getTestDBInstance();

const userId = 'user-db';
const userEmail = 'user@example.com';
const userModel = new UserModel(serverDB, userId);

beforeEach(async () => {
  await serverDB.delete(users);
  await serverDB.delete(userSettings);
  process.env.KEY_VAULTS_SECRET = 'ofQiJCXLF8mYemwfMWLOHoHimlPu91YmLfU7YZ4lreQ=';
});

afterEach(async () => {
  await serverDB.delete(users);
  await serverDB.delete(userSettings);
  process.env.KEY_VAULTS_SECRET = undefined;
});

describe('UserModel', () => {
  describe('createUser', () => {
    it('should create a new user and inbox session', async () => {
      const params = {
        id: userId,
        username: 'testuser',
        email: 'test@example.com',
      };

      await UserModel.createUser(serverDB, params);

      const user = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
      expect(user).not.toBeNull();
      expect(user?.username).toBe('testuser');
      expect(user?.email).toBe('test@example.com');

      const sessionModel = new SessionModel(serverDB, userId);
      const inbox = await sessionModel.findByIdOrSlug(INBOX_SESSION_ID);
      expect(inbox).not.toBeNull();
    });
  });

  describe('deleteUser', () => {
    it('should delete a user', async () => {
      await serverDB.insert(users).values({ id: userId });

      await UserModel.deleteUser(serverDB, userId);

      const user = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
      expect(user).toBeUndefined();
    });
  });

  describe('findById', () => {
    it('should find a user by ID', async () => {
      await serverDB.insert(users).values({ id: userId, username: 'testuser' });

      const user = await UserModel.findById(serverDB, userId);

      expect(user).not.toBeNull();
      expect(user?.id).toBe(userId);
      expect(user?.username).toBe('testuser');
    });
  });

  describe('findByEmail', () => {
    it('should find a user by email', async () => {
      await serverDB.insert(users).values({ id: userId, email: userEmail });

      const user = await UserModel.findByEmail(serverDB, userEmail);

      expect(user).not.toBeNull();
      expect(user?.id).toBe(userId);
      expect(user?.email).toBe(userEmail);
    });
  });

  describe('getUserState', () => {
    it('should get user state with decrypted keyVaults', async () => {
      const preference = { useCmdEnterToSend: true } as UserPreference;
      const keyVaults = { apiKey: 'secret' };

      await serverDB.insert(users).values({ id: userId, preference });

      const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
      const encryptedKeyVaults = await gateKeeper.encrypt(JSON.stringify(keyVaults));

      await serverDB.insert(userSettings).values({
        id: userId,
        keyVaults: encryptedKeyVaults,
      });

      const state = await userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults);

      expect(state.userId).toBe(userId);
      expect(state.preference).toEqual(preference);
      expect(state.settings.keyVaults).toEqual(keyVaults);
    });

    it('should throw an error if user not found', async () => {
      const userModel = new UserModel(serverDB, 'invalid-user-id');

      await expect(userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults)).rejects.toThrow(
        'user not found',
      );
    });
  });

  describe('updateUser', () => {
    it('should update user fields', async () => {
      await serverDB.insert(users).values({ id: userId, username: 'oldname' });

      await userModel.updateUser({ username: 'newname' });

      const updatedUser = await serverDB.query.users.findFirst({
        where: eq(users.id, userId),
      });
      expect(updatedUser?.username).toBe('newname');
    });
  });

  describe('getUserSettings', () => {
    it('should get user settings', async () => {
      await serverDB.insert(users).values({ id: userId });
      await serverDB.insert(userSettings).values({ id: userId, general: { language: 'en-US' } });

      const data = await userModel.getUserSettings();

      expect(data).toMatchObject({ id: userId, general: { language: 'en-US' } });
    });
  });

  describe('deleteSetting', () => {
    it('should delete user settings', async () => {
      await serverDB.insert(users).values({ id: userId });
      await serverDB.insert(userSettings).values({ id: userId });

      await userModel.deleteSetting();

      const settings = await serverDB.query.userSettings.findFirst({
        where: eq(users.id, userId),
      });

      expect(settings).toBeUndefined();
    });
  });

  describe('updateSetting', () => {
    it('should update user settings with new item', async () => {
      const settings = {
        general: { language: 'en-US' },
      } as UserSettingsItem;
      await serverDB.insert(users).values({ id: userId });

      await userModel.updateSetting(settings);

      const updatedSettings = await serverDB.query.userSettings.findFirst({
        where: eq(users.id, userId),
      });
      expect(updatedSettings?.general).toEqual(settings.general);
    });

    it('should update user settings with exist item', async () => {
      const settings = {
        general: { language: 'en-US' },
      } as UserSettingsItem;
      await serverDB.insert(users).values({ id: userId });
      await serverDB.insert(userSettings).values({ ...settings, keyVaults: '', id: userId });

      const newSettings = {
        general: { fontSize: 16, language: 'zh-CN', themeMode: 'dark' },
      } as UserSettingsItem;
      await userModel.updateSetting(newSettings);

      const updatedSettings = await serverDB.query.userSettings.findFirst({
        where: eq(users.id, userId),
      });
      expect(updatedSettings?.general).toEqual(newSettings.general);
    });
  });

  describe('updatePreference', () => {
    it('should update user preference', async () => {
      const preference = { guide: { topic: false } } as UserPreference;
      await serverDB.insert(users).values({ id: userId, preference });

      const newPreference: Partial<UserPreference> = {
        guide: { topic: true, moveSettingsToAvatar: true },
      };
      await userModel.updatePreference(newPreference);

      const updatedUser = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
      expect(updatedUser?.preference).toEqual({ ...preference, ...newPreference });
    });
  });

  describe('updateGuide', () => {
    it('should update user guide', async () => {
      const preference = { guide: { topic: false } } as UserGuide;
      await serverDB.insert(users).values({ id: userId, preference });

      const newGuide: Partial<UserGuide> = {
        topic: true,
        moveSettingsToAvatar: true,
        uploadFileInKnowledgeBase: true,
      };
      await userModel.updateGuide(newGuide);

      const updatedUser = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
      expect(updatedUser?.preference).toEqual({ ...preference, guide: newGuide });
    });
  });

  describe('getUserApiKeys', () => {
    it('should get and decrypt user API keys', async () => {
      const keyVaults = { openai: { apiKey: 'test-key' } };
      const gateKeeper = await KeyVaultsGateKeeper.initWithEnvKey();
      const encryptedKeyVaults = await gateKeeper.encrypt(JSON.stringify(keyVaults));

      const userId = 'user-api-id';

      await serverDB.insert(users).values({ id: userId });
      await serverDB.insert(userSettings).values({
        id: userId,
        keyVaults: encryptedKeyVaults,
      });

      const result = await UserModel.getUserApiKeys(
        serverDB,
        userId,
        KeyVaultsGateKeeper.getUserKeyVaults,
      );
      expect(result).toEqual(keyVaults);
    });

    it('should throw error when user not found', async () => {
      await expect(
        UserModel.getUserApiKeys(serverDB, 'non-existent-id', KeyVaultsGateKeeper.getUserKeyVaults),
      ).rejects.toThrow('user not found');
    });

    it('should handle decrypt failure and return empty object', async () => {
      const userId = 'user-api-test-id';
      // 模拟解密失败的情况
      const invalidEncryptedData = 'invalid:-encrypted-:data';
      await serverDB.insert(users).values({ id: userId });
      await serverDB.insert(userSettings).values({
        id: userId,
        keyVaults: invalidEncryptedData,
      });

      const result = await UserModel.getUserApiKeys(
        serverDB,
        userId,
        KeyVaultsGateKeeper.getUserKeyVaults,
      );
      expect(result).toEqual({});
    });
  });

  describe('getUserRegistrationDuration', () => {
    it('should return default values when user not found', async () => {
      const duration = await userModel.getUserRegistrationDuration();
      const today = dayjs().format('YYYY-MM-DD');

      expect(duration).toEqual({
        createdAt: today,
        duration: 1,
        updatedAt: today,
      });
    });

    it('should calculate correct duration for existing user', async () => {
      // Mock the current date
      const now = new Date('2024-01-15');
      vi.setSystemTime(now);

      const createdAt = new Date('2024-01-10'); // 5 days ago
      await serverDB.insert(users).values({
        id: userId,
        createdAt,
      });

      const duration = await userModel.getUserRegistrationDuration();

      expect(duration).toEqual({
        createdAt: '2024-01-10',
        duration: 6, // 5 days difference + 1
        updatedAt: '2024-01-15',
      });

      vi.useRealTimers();
    });
  });

  // 补充一些边界情况的测试
  describe('edge cases', () => {
    describe('updatePreference', () => {
      it('should handle undefined preference', async () => {
        await serverDB.insert(users).values({ id: userId });

        const newPreference: Partial<UserPreference> = {
          guide: { topic: true },
        };

        await userModel.updatePreference(newPreference);

        const updatedUser = await serverDB.query.users.findFirst({
          where: eq(users.id, userId),
        });

        expect(updatedUser?.preference).toMatchObject(newPreference);
      });

      it('should do nothing if user not found', async () => {
        const nonExistentUserModel = new UserModel(serverDB, 'non-existent-id');
        const result = await nonExistentUserModel.updatePreference({ guide: { topic: true } });
        expect(result).toBeUndefined();
      });
    });

    describe('updateGuide', () => {
      it('should handle undefined guide', async () => {
        await serverDB.insert(users).values({
          id: userId,
          preference: {} as UserPreference,
        });

        const newGuide: Partial<UserGuide> = {
          topic: true,
        };

        await userModel.updateGuide(newGuide);

        const updatedUser = await serverDB.query.users.findFirst({
          where: eq(users.id, userId),
        });
        expect(updatedUser?.preference).toEqual({ guide: newGuide });
      });

      it('should do nothing if user not found', async () => {
        const nonExistentUserModel = new UserModel(serverDB, 'non-existent-id');
        const result = await nonExistentUserModel.updateGuide({ topic: true });
        expect(result).toBeUndefined();
      });
    });

    describe('createUser', () => {
      it('should not create duplicate user with same id', async () => {
        const params = {
          id: userId,
          username: 'existinguser',
          email: 'existing@example.com',
        };

        // First creation
        await UserModel.createUser(serverDB, params);

        // Attempt to create with same ID but different details
        await UserModel.createUser(serverDB, {
          ...params,
          username: 'newuser',
          email: 'new@example.com',
        });

        const user = await UserModel.findById(serverDB, userId);
        expect(user?.username).toBe('existinguser');
        expect(user?.email).toBe('existing@example.com');
      });
    });

    describe('getUserState', () => {
      it('should handle empty settings', async () => {
        await serverDB.insert(users).values({
          id: userId,
          preference: {} as UserPreference,
          isOnboarded: true,
        });

        const state = await userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults);

        expect(state).toMatchObject({
          userId,
          isOnboarded: true,
          preference: {},
          settings: {
            defaultAgent: {},
            general: {},
            keyVaults: {},
            languageModel: {},
            systemAgent: {},
            tool: {},
            tts: {},
          },
        });
      });
    });
  });
});

describe('UserNotFoundError', () => {
  it('should extend TRPCError with correct code and message', () => {
    const error = new UserNotFoundError();

    expect(error).toBeInstanceOf(TRPCError);
    expect(error.code).toBe('UNAUTHORIZED');
    expect(error.message).toBe('user not found');
  });
});
