import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import fs from "node:fs";
import path from "node:path";
import { safeStorage } from "electron";
import {
  readSettings,
  resolveEffectiveSettings,
  readEffectiveSettings,
  getSettingsFilePath,
  encrypt,
  decrypt,
} from "@/main/settings";
import { getUserDataPath } from "@/paths/paths";
import { UserSettings } from "@/lib/schemas";
import { DyadError, DyadErrorKind } from "@/errors/dyad_error";
import { getRemoteDesktopConfig } from "@/ipc/shared/remote_desktop_config";

// Mock dependencies
vi.mock("node:fs");
vi.mock("node:path");
vi.mock("electron", () => ({
  safeStorage: {
    isEncryptionAvailable: vi.fn(),
    decryptString: vi.fn(),
  },
}));
vi.mock("@/paths/paths", () => ({
  getUserDataPath: vi.fn(),
}));
vi.mock("@/ipc/shared/remote_desktop_config", () => ({
  getRemoteDesktopConfig: vi.fn(),
}));

const mockFs = vi.mocked(fs);
const mockPath = vi.mocked(path);
const mockSafeStorage = vi.mocked(safeStorage);
const mockGetUserDataPath = vi.mocked(getUserDataPath);
const mockGetRemoteDesktopConfig = vi.mocked(getRemoteDesktopConfig);

describe("readSettings", () => {
  const mockUserDataPath = "/mock/user/data";
  const mockSettingsPath = "/mock/user/data/user-settings.json";

  beforeEach(() => {
    vi.clearAllMocks();
    mockGetUserDataPath.mockReturnValue(mockUserDataPath);
    mockPath.join.mockReturnValue(mockSettingsPath);
    mockSafeStorage.isEncryptionAvailable.mockReturnValue(true);
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  describe("when settings file does not exist", () => {
    it("should create default settings file and return default settings", () => {
      mockFs.existsSync.mockReturnValue(false);
      mockFs.writeFileSync.mockImplementation(() => {});

      const result = readSettings();

      expect(mockFs.existsSync).toHaveBeenCalledWith(mockSettingsPath);
      expect(mockFs.writeFileSync).toHaveBeenCalledWith(
        mockSettingsPath,
        expect.stringContaining('"selectedModel"'),
      );
      expect(scrubSettings(result)).toMatchInlineSnapshot(`
        {
          "autoExpandPreviewPanel": true,
          "enableAutoFixProblems": false,
          "enableAutoUpdate": true,
          "enableContextCompaction": true,
          "enableNativeGit": true,
          "enableProLazyEditsMode": true,
          "enableProSmartFilesContextMode": true,
          "experiments": {},
          "hasRunBefore": false,
          "isRunning": false,
          "language": "zh-CN",
          "lastKnownPerformance": undefined,
          "previewIdleTimeoutPolicy": "default",
          "providerSettings": {},
          "releaseChannel": "stable",
          "selectedChatMode": "build",
          "selectedModel": {
            "name": "auto",
            "provider": "auto",
          },
          "selectedTemplateId": "react",
          "selectedThemeId": "default",
          "telemetryConsent": "unset",
          "telemetryUserId": "[scrubbed]",
        }
      `);
    });
  });

  describe("when settings file exists", () => {
    it("should read and merge settings with defaults", () => {
      const mockFileContent = {
        selectedModel: {
          name: "gpt-4",
          provider: "openai",
        },
        telemetryConsent: "opted_in",
        hasRunBefore: true,
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      expect(mockFs.readFileSync).toHaveBeenCalledWith(
        mockSettingsPath,
        "utf-8",
      );
      expect(result.selectedModel).toEqual({
        name: "gpt-4",
        provider: "openai",
      });
      expect(result.telemetryConsent).toBe("opted_in");
      expect(result.hasRunBefore).toBe(true);
      // Should still have defaults for missing properties
      expect(result.blockUnsafeNpmPackages).toBeUndefined();
      expect(result.enableAutoUpdate).toBe(true);
      expect(result.releaseChannel).toBe("stable");
    });

    it("should decrypt encrypted provider API keys", () => {
      const mockFileContent = {
        providerSettings: {
          openai: {
            apiKey: {
              value: "encrypted-api-key",
              encryptionType: "electron-safe-storage",
            },
          },
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
      mockSafeStorage.decryptString.mockReturnValue("decrypted-api-key");

      const result = readSettings();

      expect(mockSafeStorage.decryptString).toHaveBeenCalledWith(
        Buffer.from("encrypted-api-key", "base64"),
      );
      expect(result.providerSettings.openai.apiKey).toEqual({
        value: "decrypted-api-key",
        encryptionType: "electron-safe-storage",
      });
    });

    it("should decrypt encrypted GitHub access token", () => {
      const mockFileContent = {
        githubAccessToken: {
          value: "encrypted-github-token",
          encryptionType: "electron-safe-storage",
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
      mockSafeStorage.decryptString.mockReturnValue("decrypted-github-token");

      const result = readSettings();

      expect(mockSafeStorage.decryptString).toHaveBeenCalledWith(
        Buffer.from("encrypted-github-token", "base64"),
      );
      expect(result.githubAccessToken).toEqual({
        value: "decrypted-github-token",
        encryptionType: "electron-safe-storage",
      });
    });

    it("should decrypt encrypted Supabase tokens", () => {
      const mockFileContent = {
        supabase: {
          accessToken: {
            value: "encrypted-access-token",
            encryptionType: "electron-safe-storage",
          },
          refreshToken: {
            value: "encrypted-refresh-token",
            encryptionType: "electron-safe-storage",
          },
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
      mockSafeStorage.decryptString
        .mockReturnValueOnce("decrypted-refresh-token")
        .mockReturnValueOnce("decrypted-access-token");

      const result = readSettings();

      expect(mockSafeStorage.decryptString).toHaveBeenCalledTimes(2);
      expect(result.supabase?.refreshToken).toEqual({
        value: "decrypted-refresh-token",
        encryptionType: "electron-safe-storage",
      });
      expect(result.supabase?.accessToken).toEqual({
        value: "decrypted-access-token",
        encryptionType: "electron-safe-storage",
      });
    });

    it("should handle plaintext secrets without decryption", () => {
      const mockFileContent = {
        githubAccessToken: {
          value: "plaintext-token",
          encryptionType: "plaintext",
        },
        providerSettings: {
          openai: {
            apiKey: {
              value: "plaintext-api-key",
              encryptionType: "plaintext",
            },
          },
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      expect(mockSafeStorage.decryptString).not.toHaveBeenCalled();
      expect(result.githubAccessToken?.value).toBe("plaintext-token");
      expect(result.providerSettings.openai.apiKey?.value).toBe(
        "plaintext-api-key",
      );
    });

    it("should trim whitespace from decrypted API keys", () => {
      const mockFileContent = {
        providerSettings: {
          openai: {
            apiKey: {
              value: "encrypted-api-key",
              encryptionType: "electron-safe-storage",
            },
          },
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
      mockSafeStorage.decryptString.mockReturnValue(
        "  decrypted-api-key-with-spaces\n",
      );

      const result = readSettings();

      expect(result.providerSettings.openai.apiKey).toEqual({
        value: "decrypted-api-key-with-spaces",
        encryptionType: "electron-safe-storage",
      });
    });

    it("should trim whitespace from plaintext secrets", () => {
      const mockFileContent = {
        githubAccessToken: {
          value: "  plaintext-token-with-spaces\n",
          encryptionType: "plaintext",
        },
        providerSettings: {
          openai: {
            apiKey: {
              value: "\nplaintext-api-key\n",
              encryptionType: "plaintext",
            },
          },
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      expect(result.githubAccessToken?.value).toBe(
        "plaintext-token-with-spaces",
      );
      expect(result.providerSettings.openai.apiKey?.value).toBe(
        "plaintext-api-key",
      );
    });

    it("should handle secrets without encryptionType", () => {
      const mockFileContent = {
        githubAccessToken: {
          value: "token-without-encryption-type",
        },
        providerSettings: {
          openai: {
            apiKey: {
              value: "api-key-without-encryption-type",
            },
          },
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      expect(mockSafeStorage.decryptString).not.toHaveBeenCalled();
      expect(result.githubAccessToken?.value).toBe(
        "token-without-encryption-type",
      );
      expect(result.providerSettings.openai.apiKey?.value).toBe(
        "api-key-without-encryption-type",
      );
    });

    it("should migrate deprecated 'agent' chat mode to 'build'", () => {
      const mockFileContent = {
        selectedModel: {
          name: "gpt-4",
          provider: "openai",
        },
        selectedChatMode: "agent",
        defaultChatMode: "agent",
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      // "agent" should be migrated to "build"
      expect(result.selectedChatMode).toBe("build");
      expect(result.defaultChatMode).toBe("build");
    });

    it("should preserve non-deprecated chat modes", () => {
      const mockFileContent = {
        selectedModel: {
          name: "gpt-4",
          provider: "openai",
        },
        selectedChatMode: "local-agent",
        defaultChatMode: "ask",
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      expect(result.selectedChatMode).toBe("local-agent");
      expect(result.defaultChatMode).toBe("ask");
    });

    it("should migrate deprecated 'agent' chat mode to 'build'", () => {
      const mockFileContent = {
        selectedModel: {
          name: "gpt-4",
          provider: "openai",
        },
        selectedChatMode: "agent",
        defaultChatMode: "agent",
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      // "agent" should be converted to "build" on read
      expect(result.selectedChatMode).toBe("build");
      expect(result.defaultChatMode).toBe("build");
    });

    it("should preserve non-deprecated chat modes during migration", () => {
      const mockFileContent = {
        selectedModel: {
          name: "gpt-4",
          provider: "openai",
        },
        selectedChatMode: "local-agent",
        defaultChatMode: "ask",
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      // non-deprecated modes should be preserved
      expect(result.selectedChatMode).toBe("local-agent");
      expect(result.defaultChatMode).toBe("ask");
    });

    it("should preserve extra fields not recognized by the schema", () => {
      const mockFileContent = {
        selectedModel: {
          name: "gpt-4",
          provider: "openai",
        },
        telemetryConsent: "opted_in",
        hasRunBefore: true,
        // Extra fields that are not in the schema (should be preserved)
        unknownField: "should be preserved",
        deprecatedSetting: true,
        extraConfig: {
          someValue: 123,
          anotherValue: "test",
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      expect(mockFs.readFileSync).toHaveBeenCalledWith(
        mockSettingsPath,
        "utf-8",
      );
      expect(result.selectedModel).toEqual({
        name: "gpt-4",
        provider: "openai",
      });
      expect(result.telemetryConsent).toBe("opted_in");
      expect(result.hasRunBefore).toBe(true);

      // Extra fields should be preserved by passthrough()
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const resultAny = result as any;
      expect(resultAny.unknownField).toBe("should be preserved");
      expect(resultAny.deprecatedSetting).toBe(true);
      expect(resultAny.extraConfig).toEqual({
        someValue: 123,
        anotherValue: "test",
      });

      // Should still have defaults for missing properties
      expect(result.enableAutoUpdate).toBe(true);
      expect(result.releaseChannel).toBe("stable");
    });
  });

  describe("error handling", () => {
    it("should return default settings when file read fails", () => {
      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockImplementation(() => {
        throw new DyadError("File read error", DyadErrorKind.External);
      });

      const result = readSettings();

      expect(scrubSettings(result)).toMatchInlineSnapshot(`
        {
          "autoExpandPreviewPanel": true,
          "enableAutoFixProblems": false,
          "enableAutoUpdate": true,
          "enableContextCompaction": true,
          "enableNativeGit": true,
          "enableProLazyEditsMode": true,
          "enableProSmartFilesContextMode": true,
          "experiments": {},
          "hasRunBefore": false,
          "isRunning": false,
          "language": "zh-CN",
          "lastKnownPerformance": undefined,
          "previewIdleTimeoutPolicy": "default",
          "providerSettings": {},
          "releaseChannel": "stable",
          "selectedChatMode": "build",
          "selectedModel": {
            "name": "auto",
            "provider": "auto",
          },
          "selectedTemplateId": "react",
          "selectedThemeId": "default",
          "telemetryConsent": "unset",
          "telemetryUserId": "[scrubbed]",
        }
      `);
    });

    it("should return default settings when JSON parsing fails", () => {
      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue("invalid json");

      const result = readSettings();

      expect(result).toMatchObject({
        selectedModel: {
          name: "auto",
          provider: "auto",
        },
        releaseChannel: "stable",
      });
    });

    it("should return default settings when schema validation fails", () => {
      const mockFileContent = {
        selectedModel: {
          name: "gpt-4",
          // Missing required 'provider' field
        },
        releaseChannel: "invalid-channel", // Invalid enum value
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));

      const result = readSettings();

      expect(result).toMatchObject({
        selectedModel: {
          name: "auto",
          provider: "auto",
        },
        releaseChannel: "stable",
      });
    });

    it("should handle decryption errors gracefully", () => {
      const mockFileContent = {
        githubAccessToken: {
          value: "corrupted-encrypted-data",
          encryptionType: "electron-safe-storage",
        },
      };

      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
      mockSafeStorage.decryptString.mockImplementation(() => {
        throw new DyadError("Decryption failed", DyadErrorKind.External);
      });

      const result = readSettings();

      expect(result).toMatchObject({
        selectedModel: {
          name: "auto",
          provider: "auto",
        },
        releaseChannel: "stable",
      });
    });
  });

  describe("effective settings", () => {
    it("applies the remote default when the user has not explicitly set the setting", async () => {
      mockGetRemoteDesktopConfig.mockResolvedValue({
        defaults: { blockUnsafeNpmPackages: false },
      });
      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify({}));

      const result = await readEffectiveSettings();

      expect(result.blockUnsafeNpmPackages).toBe(false);
      expect(mockFs.writeFileSync).not.toHaveBeenCalled();
    });

    it("does not override an explicitly stored local value", () => {
      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify({}));

      const result = resolveEffectiveSettings(
        {
          ...readSettings(),
          blockUnsafeNpmPackages: true,
        },
        null,
      );

      expect(result.blockUnsafeNpmPackages).toBe(true);
    });

    it("falls back to the built-in default when remote config is missing", () => {
      mockFs.existsSync.mockReturnValue(true);
      mockFs.readFileSync.mockReturnValue(JSON.stringify({}));

      const result = resolveEffectiveSettings(readSettings(), null);

      expect(result.blockUnsafeNpmPackages).toBe(true);
    });
  });

  describe("getSettingsFilePath", () => {
    it("should return correct settings file path", () => {
      const result = getSettingsFilePath();

      expect(mockGetUserDataPath).toHaveBeenCalled();
      expect(mockPath.join).toHaveBeenCalledWith(
        mockUserDataPath,
        "user-settings.json",
      );
      expect(result).toBe(mockSettingsPath);
    });
  });
});

describe("encrypt", () => {
  it("should trim whitespace before encrypting", () => {
    const result = encrypt("  my-api-key\n");
    // In test builds, encryption falls back to plaintext
    expect(result.value).toBe("my-api-key");
  });

  it("should trim trailing newlines", () => {
    const result = encrypt("sk-abc123\n\n");
    expect(result.value).toBe("sk-abc123");
  });

  it("should not alter values without whitespace", () => {
    const result = encrypt("sk-abc123");
    expect(result.value).toBe("sk-abc123");
  });
});

describe("decrypt", () => {
  it("should trim whitespace from plaintext secrets", () => {
    const result = decrypt({
      value: "  my-api-key\n",
      encryptionType: "plaintext",
    });
    expect(result).toBe("my-api-key");
  });

  it("should trim whitespace from electron-safe-storage secrets", () => {
    mockSafeStorage.decryptString.mockReturnValue("  decrypted-key\n");
    const result = decrypt({
      value: Buffer.from("encrypted").toString("base64"),
      encryptionType: "electron-safe-storage",
    });
    expect(result).toBe("decrypted-key");
  });

  it("should not alter values without whitespace", () => {
    const result = decrypt({
      value: "sk-abc123",
      encryptionType: "plaintext",
    });
    expect(result).toBe("sk-abc123");
  });
});

function scrubSettings(result: UserSettings) {
  return {
    ...result,
    telemetryUserId: "[scrubbed]",
  };
}
