/**
 * Copyright IBM Corp. 2024, 2025
 */
import { LogWrapper } from '../../service/log-wrapper.js';
import { RestHandler } from '../protocol/rest-handler.js';
import { Request, Test } from '../../schemas/test.schema.js';
import { AssertionEngine } from '../assertion/assertion.engine.js';
import { VCM } from '../variable-context-manager/context-manager.js';
import { compileExpression } from 'filtrex';
import { convertToExecutableFormat } from '../../helpers/condition-converter.js';
import {
  AssertionSummary,
  Header,
  TestExecutionResult,
  RunExecutionAssertion,
} from '../../models/interface.js';
import { TestExecutionReport } from '../reporting/test-execution-report.js';
import { sanitizeAxiosResponse } from '../../helpers/helper.js';

export class TestRunner {
  private readonly test: Test;

  constructor(test: Test) {
    this.test = test;
  }

  public async run() {
    const {
      metadata: { name, type },
      spec: { request: requests },
      vcmId,
    } = this.test;
    LogWrapper.logInfo('0215', `Starting Test run: ${name}`);
    const assertionSummary: AssertionSummary[] = [];
    const executions: TestExecutionResult[] = [];
    const startedAt = Date.now();
    let assertionResults: any[] = [];
    let isStopOnFailTriggered = false;
    for (const request of requests) {
      if (request.skipped) {
        continue;
      }
      let requestCondition = true;
      // Check whether request condition is met to execute
      if (request.if !== undefined && typeof request.if !== 'boolean') {
        try {
          // Convert human-readable format to executable format
          const executableCondition = convertToExecutableFormat(request.if);
          // Use the same approach as in assertion.engine.ts
          const resolvedExpression = this.resolveConditionExpression(
            executableCondition,
            vcmId!,
          );
          const exp = compileExpression(resolvedExpression);
          requestCondition = exp(resolvedExpression);
        } catch (error) {
          console.error(error);
          requestCondition = false;
        }
      }

      if (requestCondition) {
        const step = { ...request, endpoint: this.constructUrl(request) };
        const executionStartedAt = Date.now();
        let executionCompletedAt;
        try {
          await new RestHandler().execute(step, vcmId!);
          executionCompletedAt = Date.now();
        } catch {
          executionCompletedAt = Date.now();
        }
        // construction execution result
        const constructedRequest = VCM.resolve(vcmId!, '${request}');
        const response = VCM.resolve(vcmId!, '${response}');
        const requestHeaders = VCM.resolve(vcmId!, '${requestHeaders}');
        const headers = response?.headers || response?.response?.headers;
        response.headers = Object.entries(headers).map(([key, value]) => ({
          key,
          value,
        }));

        if (type === 'api-call') {
          return sanitizeAxiosResponse(response);
        }

        const managedRequestHeaders = Object.entries(requestHeaders).map(
          ([key, value]) =>
            ({
              key,
              value,
            }) as Header,
        );
        executions.push({
          id: '', // TODO
          itemName: `${request.method} ${request.resource}`,
          response: response,
          request: {
            ...step,
            endpoint: constructedRequest?.url ?? step.endpoint,
            headers: managedRequestHeaders,
          },
          startedAt: executionStartedAt,
          completedAt: executionCompletedAt,
          assertions: [],
        });

        // Pass assertions[] to assert engine
        assertionResults = [];
        isStopOnFailTriggered = false;
        if (request.assertions) {
          if (Array.isArray(request.assertions)) {
            // Format 1: array of objects with $ref
            for (const assertion of request.assertions) {
              if (assertion) {
                const [result, stopOnFailTriggered] =
                  await new AssertionEngine().assert(assertion, vcmId!);
                if (result && Array.isArray(result)) {
                  assertionResults.push(...result);
                }

                // If stopOnFail was triggered, mark remaining requests as cancelled
                if (stopOnFailTriggered) {
                  isStopOnFailTriggered = true;
                  // Ensure current request's assertions are included in the results
                  executions[executions.length - 1].assertions = [
                    ...assertionResults,
                  ];
                  assertionSummary.push({
                    request: request.resource,
                    assertions: [...assertionResults],
                  });

                  // Mark all remaining requests as cancelled
                  for (
                    let i = requests.indexOf(request) + 1;
                    i < requests.length;
                    i++
                  ) {
                    const cancelledExecution = this.createCancelledExecution(
                      requests[i],
                    );
                    executions.push(cancelledExecution);

                    assertionSummary.push({
                      request: requests[i].resource,
                      assertions: cancelledExecution.assertions,
                    });

                    // Mark as skipped to avoid processing in the main loop
                    requests[i].skipped = true;
                  }
                }
              }
            }
          } else {
            // Format 2: single assertion with direct $ref property
            const [result, stopOnFailTriggered] =
              await new AssertionEngine().assert(request.assertions, vcmId!);
            if (result && Array.isArray(result)) {
              assertionResults.push(...result);
            }

            // If stopOnFail was triggered, mark remaining requests as cancelled
            if (stopOnFailTriggered) {
              isStopOnFailTriggered = true;
              // Ensure current request's assertions are included in the results
              executions[executions.length - 1].assertions = [
                ...assertionResults,
              ];
              assertionSummary.push({
                request: request.resource,
                assertions: [...assertionResults],
              });

              // Mark all remaining requests as cancelled
              for (
                let i = requests.indexOf(request) + 1;
                i < requests.length;
                i++
              ) {
                const cancelledExecution = this.createCancelledExecution(
                  requests[i],
                );
                executions.push(cancelledExecution);

                assertionSummary.push({
                  request: requests[i].resource,
                  assertions: cancelledExecution.assertions,
                });

                // Mark as skipped to avoid processing in the main loop
                requests[i].skipped = true;
              }
            }
          }
        }
        // Create a deep copy of the assertion results to avoid reference issues
        // Only add to results if stopOnFail wasn't triggered (otherwise already added)
        if (!isStopOnFailTriggered) {
          executions[executions.length - 1].assertions = [...assertionResults];
          // TODO check the below array is really needed.
          assertionSummary.push({
            request: request.resource,
            assertions: [...assertionResults],
          });
        }
      } else {
        // Mark current request's remaining assertions as cancelled if any
        if (request.assertions) {
          const cancelledExecution = this.createCancelledExecution(request);
          executions.push(cancelledExecution);

          assertionSummary.push({
            request: request.resource,
            assertions: cancelledExecution.assertions,
          });
        }
      }
    }
    const completedAt = Date.now();

    let envMetadata:
      | { name: string; version: string; namespace: string }
      | undefined;
    const env = this.test.spec.environment;

    if (env && !Array.isArray(env) && 'variables' in env) {
      envMetadata = env.variables?.[0]?.metadata;
    }

    const report = new TestExecutionReport().collectReport(
      vcmId!,
      this.test.metadata.name,
      assertionSummary,
      executions,
      startedAt,
      completedAt,
      envMetadata,
    );
    // Clean up memory after every test.
    VCM.deleteContext(vcmId!);
    LogWrapper.logInfo('0215', `Completed Test run: ${name}`);

    return report;
  }

  private constructUrl(request: Request): string {
    const { endpoint, resource, parameters } = request;
    const {
      spec: {
        api: { $endpoint },
      },
    } = this.test;

    // if any endpoint is passed within request that will get precedence.
    const url = (endpoint ?? $endpoint)!;

    // Replace path parameters in the resource path
    let processedResource = resource;

    // Check if parameters exist and process path parameters
    if (parameters && Array.isArray(parameters)) {
      for (const param of parameters) {
        if (param.key && param.value !== undefined) {
          // Replace {paramName} with actual value
          const paramPattern = new RegExp(`\\{${param.key}\\}`, 'g');
          processedResource = processedResource.replace(
            paramPattern,
            param.value.toString(),
          );
        }
      }
    }

    return `${url}${processedResource}`;
  }

  /**
   * Creates cancelled assertion results for assertions that weren't executed
   * @param assertionsParam - The assertions that need to be marked as cancelled
   * @returns Array of cancelled assertion results
   */
  private createCancelledAssertions(
    assertionsParam: any,
  ): RunExecutionAssertion[] {
    if (!assertionsParam) {
      return [];
    }

    // Normalize input to handle nested assertions property
    const normalizedAssertions =
      'assertions' in assertionsParam &&
      Array.isArray(assertionsParam.assertions)
        ? assertionsParam.assertions
        : assertionsParam;

    // Convert to array if it's a single assertion
    const assertionsArray = Array.isArray(normalizedAssertions)
      ? normalizedAssertions
      : [normalizedAssertions];

    // Map each assertion to a cancelled RunExecutionAssertion
    return assertionsArray
      .filter((assertion) => assertion !== null && assertion !== undefined)
      .flatMap((assertion) => this.createCancelledAssertion(assertion));
  }

  /**
   * Creates a single cancelled assertion result
   * @param assertion - The assertion to convert to a cancelled result
   * @returns A RunExecutionAssertion with cancelled status
   */
  private createCancelledAssertion(
    assertion: any,
  ): RunExecutionAssertion | RunExecutionAssertion[] {
    // Check if assertion.spec is an array - create cancelled assertion object for each individual item
    if (assertion.spec && Array.isArray(assertion.spec)) {
      return assertion.spec.map((spec: any) => ({
        assertion: spec.name,
        skipped: true,
        action: spec.action ?? '',
        key: spec.key ?? '',
        expectedValue: spec.value,
        actualValue: null,
        error: {
          name: 'CancelledError',
          message:
            'Test execution stopped due to previous error and stopOnFail flag',
          stack: '',
          test: assertion.metadata?.name,
        },
      }));
    }

    const assertionName = assertion.spec?.name;
    const testName = assertion.metadata?.name;

    return {
      assertion: assertionName,
      skipped: true,
      action: assertion.spec?.action ?? '',
      // Handle inconsistent property access between array and single assertion cases
      key: assertion.spec?.key ?? assertion.metadata?.key ?? '',
      expectedValue: assertion.spec?.value,
      actualValue: null,
      error: {
        name: 'CancelledError',
        message:
          'Test execution stopped due to previous error and stopOnFail flag',
        stack: '',
        test: testName,
      },
    };
  }

  /**
   * Creates a cancelled execution result for a request that wasn't executed
   * @param request - The request that wasn't executed
   * @returns TestExecutionResult with cancelled status
   */
  private createCancelledExecution(request: Request): TestExecutionResult {
    const cancelledAssertions = this.createCancelledAssertions(
      request.assertions,
    );

    return {
      id: '',
      itemName: `${request.method} ${request.resource}`,
      response: {
        headers: [],
        status: 0,
        statusText: 'Cancelled',
      },
      request: {
        ...request,
        endpoint: this.constructUrl(request),
        headers: [],
      },
      startedAt: Date.now(),
      completedAt: Date.now(),
      assertions: cancelledAssertions,
    };
  }

  /**
   * Resolves a condition expression, handling variable references properly
   * Similar to the approach used in assertion.engine.ts
   */
  private resolveConditionExpression(
    expression: string,
    contextId: string,
  ): string {
    // If the expression is already a complex expression with operators, resolve it as is
    if (
      expression.includes('==') ||
      expression.includes('!=') ||
      expression.includes('>') ||
      expression.includes('<') ||
      expression.includes('&&') ||
      expression.includes('||')
    ) {
      // Replace all ${...} patterns with their resolved values
      return expression.replace(/\$\{([^{}]*)\}/g, (match, expr) => {
        try {
          // Use the same approach as in assertion.engine.ts
          const value = this.resolveExpressionValue(expr, contextId);

          // Convert the value to a string representation suitable for filtrex
          if (value === undefined || value === null) {
            return '""'; // Empty string for undefined/null values
          } else if (typeof value === 'string') {
            return `"${value}"`; // Wrap strings in quotes
          } else if (typeof value === 'object') {
            return '""'; // Empty string for objects that can't be used in expressions
          } else {
            return String(value); // Convert numbers, booleans, etc. to string
          }
        } catch (e) {
          console.error(e);
          return '""'; // Return empty string on error
        }
      });
    }

    // If it's a simple variable reference, resolve it directly
    return VCM.resolve(contextId, expression);
  }

  /**
   * Resolves an expression value, handling path resolution properly
   */
  private resolveExpressionValue(expr: string, contextId: string): any {
    const parts = expr.split('.');
    const baseKey = parts[0];

    // Get the base value from the context
    const context = VCM.getContext(contextId);
    const global = VCM.getGlobalContext();
    let resolved = context?.getValue(baseKey) ?? global?.getValue(baseKey);

    if (resolved === undefined) {
      return undefined;
    }

    // Navigate through the path parts
    for (const part of parts.slice(1)) {
      const key = /^\d+$/.test(part) ? Number(part) : part;
      if (resolved === undefined || resolved === null || !(key in resolved)) {
        return undefined;
      }
      resolved = resolved[key];
    }

    return resolved;
  }
}
