/*
 * Copyright (c) 2024, salesforce.com, inc.
 * All rights reserved.
 * Licensed under the BSD 3-Clause license.
 * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
 */
import * as path from 'path';
import fs from 'fs';
import { EnvironmentSettings as Env } from './environmentSettings';
import * as core from './core/index';
import { ProjectConfig, ProjectShapeOption, ExtensionConfig } from './core/types';
import {
  deleteScratchOrg,
  createFolder,
  generateSfProject,
  gitRepoExists,
  getRepoNameFromUrl,
  getFolderName,
  gitClone
} from './system-operations';
import { verifyExtensionsAreRunning, reloadAndEnableExtensions, checkForUncaughtErrors } from './testing';
import { executeQuickPick, reloadWindow, verifyProjectLoaded } from './ui-interaction';
import { setUpScratchOrg } from './salesforce-components';
import { retryOperation } from './retryUtils';

export class TestSetup {
  public testSuiteSuffixName = '';
  // this needs to be defined in EnvironmentSettings with the default being os.tmpDir()
  public tempFolderPath = path.join(process.cwd(), 'e2e-temp');
  public projectFolderPath: string | undefined;
  public testDataFolderPath: string | undefined;
  public aliasAndUserNameWereVerified = false;
  public scratchOrgAliasName: string | undefined;
  public scratchOrgId: string | undefined;
  private configuredExtensions: ExtensionConfig[] = [];

  private constructor() {
    // Private constructor to prevent direct instantiation - use TestSetup.setUp() instead
  }

  public get tempProjectName(): string {
    return 'TempProject-' + this.testSuiteSuffixName;
  }

  public static async setUp(testReqConfig: core.TestReqConfig): Promise<TestSetup> {
    const testSetup = new TestSetup();
    testSetup.testSuiteSuffixName = testReqConfig.testSuiteSuffixName;
    core.log('');
    core.log(`${testSetup.testSuiteSuffixName} - Starting TestSetup.setUp()...`);

    // Configure extensions based on test requirements
    testSetup.configureExtensions(testReqConfig);

    /* The expected workspace will be open up after setUpTestingWorkspace */
    await testSetup.setUpTestingWorkspace(testReqConfig.projectConfig);
    await executeQuickPick('View: Close All Editors');
    if (testReqConfig.projectConfig.projectShape !== ProjectShapeOption.NONE) {
      await verifyExtensionsAreRunning(testSetup.getExtensionsToVerify());

      if (process.platform === 'darwin') testSetup.setJavaHomeConfigEntry(); // Extra config needed for Apex LSP on GHA
      if (testReqConfig.isOrgRequired) {
        await setUpScratchOrg(testSetup);
      }
      await reloadAndEnableExtensions(); // This is necessary in order to update JAVA home path
    }
    testSetup.setWindowDialogStyle();
    testSetup.setWorkbenchHoverDelay();
    testSetup.setMaximumWindowSize();

    await reloadWindow(); // reload window to apply settings

    core.log(`${testSetup.testSuiteSuffixName} - ...finished TestSetup.setUp()`);
    return testSetup;
  }

  /**
   * Configure extensions based on test requirements
   * @param testReqConfig Test requirement configuration
   */
  private configureExtensions(testReqConfig: core.TestReqConfig): void {
    // Use provided extensions or empty array if none provided
    this.configuredExtensions = testReqConfig.extensionConfigs || [];
    testReqConfig.extensionConfigs = testReqConfig.extensionConfigs || [];

    // If extensionConfigs is provided, configure the specific extensions
    if (testReqConfig.extensionConfigs) {
      if (testReqConfig.extensionConfigs.length === 0) {
        // Empty array means no extensions
        this.configuredExtensions = [];
      } else {
        // For each configured extension, update the corresponding default extension or add a new one
        for (const extConfig of testReqConfig.extensionConfigs) {
          const existingExtIndex = this.configuredExtensions.findIndex(
            ext => ext.extensionId === extConfig.extensionId
          );

          if (existingExtIndex >= 0) {
            // Update existing extension configuration
            this.configuredExtensions[existingExtIndex] = {
              ...this.configuredExtensions[existingExtIndex],
              shouldInstall: extConfig.shouldInstall,
              shouldVerifyActivation: extConfig.shouldVerifyActivation,
              vsixPath: extConfig.vsixPath || this.configuredExtensions[existingExtIndex].vsixPath
            };
          } else {
            // Add new extension configuration
            this.configuredExtensions.push({
              extensionId: extConfig.extensionId,
              name: extConfig.name || extConfig.extensionId, // Use provided name or extensionId as name
              vsixPath: extConfig.vsixPath || '',
              shouldInstall: extConfig.shouldInstall,
              shouldVerifyActivation: extConfig.shouldVerifyActivation
            });
          }
        }
      }
    }

    // For backward compatibility: handle excludedExtensions by setting shouldInstall to 'never'
    if (testReqConfig.excludedExtensions && testReqConfig.excludedExtensions.length > 0) {
      for (const excludedExtId of testReqConfig.excludedExtensions) {
        const extIndex = this.configuredExtensions.findIndex(ext => ext.extensionId === excludedExtId);
        if (extIndex >= 0) {
          this.configuredExtensions[extIndex].shouldInstall = 'never';
          this.configuredExtensions[extIndex].shouldVerifyActivation = false;
        }
      }
    }

    core.log(`${this.testSuiteSuffixName} - Configured ${this.configuredExtensions.length} extensions for testing`);
  }

  /**
   * Get extensions that should be verified during test setup
   */
  public getExtensionsToVerify(): ExtensionConfig[] {
    return this.configuredExtensions.filter(ext => ext.shouldVerifyActivation);
  }

  /**
   * Get extensions that should be installed with a specific installation option
   * @param option Installation option to filter by
   */
  public getExtensionsToInstall(option: 'always' | 'never' | 'optional'): ExtensionConfig[] {
    return this.configuredExtensions.filter(ext => ext.shouldInstall === option);
  }

  public async tearDown(shouldCheckForUncaughtErrors = true): Promise<void> {
    if (shouldCheckForUncaughtErrors) await checkForUncaughtErrors(this.configuredExtensions);
    try {
      await deleteScratchOrg(this.scratchOrgAliasName);
    } catch (error) {
      core.log(`Deleting scratch org (or info) failed with Error: ${(error as Error).message}`);
    }
  }

  private async initializeNewSfProject() {
    if (!fs.existsSync(this.tempFolderPath)) {
      createFolder(this.tempFolderPath);
    }
    await generateSfProject(this.tempProjectName, this.tempFolderPath); // generate a sf project for 'new'
    this.projectFolderPath = path.join(this.tempFolderPath, this.tempProjectName);
  }

  public async setUpTestingWorkspace(projectConfig: ProjectConfig) {
    core.log(`${this.testSuiteSuffixName} - Starting setUpTestingWorkspace()...`);
    let projectName: string | null;
    switch (projectConfig.projectShape) {
      case ProjectShapeOption.NEW:
        await this.initializeNewSfProject();
        break;

      case ProjectShapeOption.NAMED:
        if (projectConfig.githubRepoUrl) {
          // verify if folder matches the github repo url
          const repoExists = await gitRepoExists(projectConfig.githubRepoUrl);
          if (!repoExists) {
            this.throwError(`Repository does not exist or is inaccessible: ${projectConfig.githubRepoUrl}`);
          }
          const repoName = getRepoNameFromUrl(projectConfig.githubRepoUrl);
          if (!repoName) {
            this.throwError(`Unable to determine repository name from URL: ${projectConfig.githubRepoUrl}`);
          } else {
            projectName = repoName;
            if (projectConfig.folderPath) {
              const localProjName = getFolderName(projectConfig.folderPath);
              if (localProjName !== repoName) {
                this.throwError(
                  `The local project ${localProjName} does not match the required Github repo ${repoName}`
                );
              } else {
                // If it is a match, use the local folder directly. Local dev use only.
                this.projectFolderPath = projectConfig.folderPath;
              }
            } else {
              // Clone the project from Github URL directly
              this.projectFolderPath = path.join(this.tempFolderPath, repoName);
              await gitClone(projectConfig.githubRepoUrl, this.projectFolderPath);
            }
          }
        } else {
          // missing info, throw an error
          this.throwError(`githubRepoUrl is required for named project shape`);
        }
        break;

      case ProjectShapeOption.ANY:
        // ANY: workspace is designated to open when redhat vscode-extension-tester is initialized
        if (projectConfig.folderPath) {
          this.projectFolderPath = projectConfig.folderPath;
          projectName = getFolderName(projectConfig.folderPath);
        } else {
          // Fallback: if no folder specified, create a new sf project instead
          await this.initializeNewSfProject();
        }
        break;

      case ProjectShapeOption.NONE:
        // NONE: no project open in the workspace by default
        /* create the e2e-temp folder to benefit further testing */
        this.projectFolderPath = path.join(this.tempFolderPath, this.tempProjectName);
        if (!fs.existsSync(this.tempFolderPath)) {
          createFolder(this.tempFolderPath);
        }
        break;

      default:
        this.throwError(`Invalid project shape: ${projectConfig.projectShape}`);
    }

    if ([ProjectShapeOption.NAMED, ProjectShapeOption.NEW].includes(projectConfig.projectShape)) {
      core.log(`Project folder to open: ${this.projectFolderPath}`);
      // Verify the project was loaded.
      await retryOperation(async () => {
        if (this.projectFolderPath) {
          await core.openFolder(this.projectFolderPath);
          await verifyProjectLoaded(projectName ?? this.tempProjectName);
        } else {
          this.throwError('Project folder path is not set');
        }
      });
    }
  }

  private throwError(message: string) {
    core.log(message);
    throw new Error(message);
  }

  public updateScratchOrgDefWithEdition(scratchOrgEdition: core.OrgEdition) {
    if (scratchOrgEdition === 'enterprise') {
      if (!this.projectFolderPath) {
        this.throwError('Project folder path is not set');
        return; // This line will never be reached, but helps TypeScript understand
      }
      const projectScratchDefPath = path.join(this.projectFolderPath, 'config', 'project-scratch-def.json');
      let projectScratchDef = fs.readFileSync(projectScratchDefPath, 'utf8');
      projectScratchDef = projectScratchDef.replace(`"edition": "Developer"`, `"edition": "Enterprise"`);
      fs.writeFileSync(projectScratchDefPath, projectScratchDef, 'utf8');
    }
  }

  private setJavaHomeConfigEntry(): void {
    if (!this.projectFolderPath) {
      this.throwError('Project folder path is not set');
      return; // This line will never be reached, but helps TypeScript understand
    }
    const vscodeSettingsPath = path.join(this.projectFolderPath, '.vscode', 'settings.json');
    if (!Env.getInstance().javaHome) {
      return;
    }
    if (!fs.existsSync(path.dirname(vscodeSettingsPath))) {
      fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
    }

    let settings = fs.existsSync(vscodeSettingsPath) ? JSON.parse(fs.readFileSync(vscodeSettingsPath, 'utf8')) : {};

    settings = {
      ...settings,
      ...(process.env.JAVA_HOME ? { 'salesforcedx-vscode-apex.java.home': process.env.JAVA_HOME } : {})
    };
    fs.writeFileSync(vscodeSettingsPath, JSON.stringify(settings, null, 2), 'utf8');
    core.log(
      `${this.testSuiteSuffixName} - Set 'salesforcedx-vscode-apex.java.home' to '${process.env.JAVA_HOME}' in ${vscodeSettingsPath}`
    );
  }

  private setWorkbenchHoverDelay(): void {
    if (!this.projectFolderPath) {
      this.throwError('Project folder path is not set');
      return; // This line will never be reached, but helps TypeScript understand
    }
    const vscodeSettingsPath = path.join(this.projectFolderPath, '.vscode', 'settings.json');

    if (!fs.existsSync(path.dirname(vscodeSettingsPath))) {
      fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
    }

    let settings = fs.existsSync(vscodeSettingsPath) ? JSON.parse(fs.readFileSync(vscodeSettingsPath, 'utf8')) : {};

    // Update settings to set workbench.hover.delay
    settings = {
      ...settings,
      'workbench.hover.delay': 300000
    };

    fs.writeFileSync(vscodeSettingsPath, JSON.stringify(settings, null, 2), 'utf8');
    core.log(`${this.testSuiteSuffixName} - Set 'workbench.hover.delay' to '300000' in ${vscodeSettingsPath}`);
  }

  private setMaximumWindowSize(): void {
    if (!this.projectFolderPath) {
      return;
    }
    const vscodeSettingsPath = path.join(this.projectFolderPath, '.vscode', 'settings.json');

    if (!fs.existsSync(path.dirname(vscodeSettingsPath))) {
      fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
    }

    let settings = fs.existsSync(vscodeSettingsPath) ? JSON.parse(fs.readFileSync(vscodeSettingsPath, 'utf8')) : {};

    // Update settings to set window.newWindowDimensions
    settings = {
      ...settings,
      'window.newWindowDimensions': 'maximized'
    };

    fs.writeFileSync(vscodeSettingsPath, JSON.stringify(settings, null, 2), 'utf8');
    core.log(`${this.testSuiteSuffixName} - Set 'window.newWindowDimensions' to 'maximized' in ${vscodeSettingsPath}`);
  }

  private setWindowDialogStyle(): void {
    if (!this.projectFolderPath) {
      this.throwError('Project folder path is not set');
      return; // This line will never be reached, but helps TypeScript understand
    }
    const vscodeSettingsPath = path.join(this.projectFolderPath, '.vscode', 'settings.json');

    if (!fs.existsSync(path.dirname(vscodeSettingsPath))) {
      fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
    }

    let settings = fs.existsSync(vscodeSettingsPath) ? JSON.parse(fs.readFileSync(vscodeSettingsPath, 'utf8')) : {};

    // Update settings to set window.dialogStyle
    settings = {
      ...settings,
      'window.dialogStyle': 'custom'
    };

    fs.writeFileSync(vscodeSettingsPath, JSON.stringify(settings, null, 2), 'utf8');
    core.log(`${this.testSuiteSuffixName} - Set 'window.dialogStyle' to 'custom' in ${vscodeSettingsPath}`);
  }
}
