/**
 * Copyright IBM Corp. 2024, 2025
 */
import { compileExpression } from 'filtrex';
import { performAssertion } from '../../handlers/assertion.handler.js';
import { RunExecutionAssertion } from '../../models/interface.js';
import { Assertions } from '../../schemas/test.schema.js';
import { VCM } from '../variable-context-manager/context-manager.js';
import { convertToExecutableFormat } from '../../helpers/condition-converter.js';

// Interface for wildcard match results
interface WildcardResult {
  isWildcardResult: true;
  matches: any[];
  originalPath: string;
}

export class AssertionEngine {
  async assert(
    assertions: Assertions,
    contextId: string,
  ): Promise<[RunExecutionAssertion[], boolean]> {
    const results: RunExecutionAssertion[] = [];
    let isStopOnFailTriggered = false;
    for (const assertion of assertions.assertions!) {
      for (const spec of assertion.spec) {
        let assertionExecutionCheck = true;
        const {
          name,
          action,
          key,
          value,
          if: conditionCheck,
          stopOnFail,
        } = spec;
        if (
          conditionCheck !== undefined &&
          typeof conditionCheck !== 'boolean'
        ) {
          try {
            // Convert human-readable format to executable format
            const executableCondition =
              convertToExecutableFormat(conditionCheck);
            const resolvedExpression = VCM.resolve(
              contextId,
              executableCondition,
            );
            const exp = compileExpression(resolvedExpression);
            assertionExecutionCheck = exp(resolvedExpression);
          } catch (error) {
            console.error(error);
            assertionExecutionCheck = false;
          }
        }
        if (assertionExecutionCheck) {
          let actualValue;
          let expectedValue;
          try {
            expectedValue = this.resolveValue(value, contextId);
            actualValue = this.resolveKey(key, contextId);

            // Handle wildcard results differently
            if (this.isWildcardResult(actualValue)) {
              this.performWildcardAssertion(
                action,
                actualValue,
                expectedValue,
                name,
              );
            } else {
              performAssertion(action, actualValue, expectedValue);
            }

            results.push({
              metadata: assertion.metadata,
              assertion: name,
              skipped: false,
              actualValue: this.isWildcardResult(actualValue)
                ? actualValue.matches
                : actualValue,
              expectedValue,
              action,
              key,
            });
          } catch (error: any) {
            results.push({
              metadata: assertion.metadata,
              assertion: name,
              skipped: false,
              error: {
                name: error?.name || 'AssertionError',
                test: name,
                message: error?.message || '',
                stack: error?.stack || '',
              },
              actualValue: this.isWildcardResult(actualValue)
                ? actualValue?.matches
                : actualValue,
              expectedValue,
              action,
              key,
            });
            if (stopOnFail) {
              // Mark remaining assertions as skipped
              const remainingAssertions = assertions.assertions!.flatMap((a) =>
                a.spec
                  .filter((s) => !results.some((r) => r.assertion === s.name))
                  .map((s) => ({
                    metadata: a.metadata,
                    assertion: s.name,
                    skipped: true,
                    error: undefined,
                    action: s.action,
                    key: s.key,
                  })),
              );
              results.push(...remainingAssertions);
              // Return a flag to indicate the test was terminated due to stop on fail
              // This flag needs to be checked in TestRunner to cancel all remaining requests
              isStopOnFailTriggered = true;
              break;
            }
          }
        } else {
          // if assertionExecutionCheck is not passed, it will skip the test.
          results.push({
            metadata: assertion.metadata,
            assertion: name,
            skipped: true,
            error: undefined,
            action,
            key,
          });
        }
      }
    }
    return [results, isStopOnFailTriggered];
  }

  // For backward compatibility we check whether key have ${ to resolve it}
  private resolveKey(key: string, contextId: string): any {
    if (key.includes('*')) {
      return this.resolveWildcardKey(key, contextId);
    }
    return this.resolveValue(this.wrapKey(key), contextId);
  }

  private wrapKey(key: string): string {
    return key.includes('${') ? key : `\${${key}}`;
  }

  private resolveValue(key: any, contextId: string): any {
    return VCM.resolve(contextId, key);
  }

  /**
   * Helper method to check if a value is a wildcard result
   */
  private isWildcardResult(value: any): value is WildcardResult {
    return (
      value && typeof value === 'object' && value.isWildcardResult === true
    );
  }

  /**
   * Performs assertions on each item in a wildcard result
   * If any assertion fails, the entire assertion fails
   */
  private performWildcardAssertion(
    action: string,
    wildcardResult: WildcardResult,
    expected: any,
    assertionName: string,
  ): void {
    if (wildcardResult.matches.length === 0) {
      throw new Error(
        `No matches found for wildcard path: ${wildcardResult.originalPath}`,
      );
    }

    // Check each match against the expected value
    for (const match of wildcardResult.matches) {
      try {
        performAssertion(action, match, expected);
      } catch (error: any) {
        throw new Error(
          `Assertion '${assertionName}' failed for path '${wildcardResult.originalPath}': ${error.message}`,
        );
      }
    }
  }

  private resolveWildcardKey(key: string, contextId: string): WildcardResult {
    const parts = key.split('.');
    const rootKey = parts[0];
    const root = this.resolveValue(this.wrapKey(rootKey), contextId);

    if (!root)
      return { isWildcardResult: true, matches: [], originalPath: key };

    const matches = this.collectWildcardMatches(root, parts.slice(1));
    return { isWildcardResult: true, matches, originalPath: key };
  }

  private collectWildcardMatches(node: any, pathParts: string[]): any[] {
    if (pathParts.length === 0) {
      return [node];
    }

    const [head, ...tail] = pathParts;
    const results: any[] = [];

    if (head === '*') {
      if (Array.isArray(node)) {
        // Iterate array items
        for (const item of node) {
          results.push(...this.collectWildcardMatches(item, tail));
        }
      } else if (
        node &&
        Object.prototype.toString.call(node) === '[object Object]'
      ) {
        // Iterate plain object values only (not arrays)
        for (const key of Object.keys(node)) {
          results.push(...this.collectWildcardMatches(node[key], tail));
        }
      }

      // Skip if node is not iterable
    } else if (node && typeof node === 'object' && head in node) {
      results.push(...this.collectWildcardMatches(node[head], tail));
    }

    return results;
  }
}
