/*
 * Copyright (c) 2010, 2025 BSI Business Systems Integration AG
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
import {
  AbstractConstructor, arrays, AutoLeafPageWithNodes, BaseDoEntity, Constructor, dataObjects, DoEntity, HybridActionEvent, HybridManager, ObjectFactory, Page, PageWithNodes, PageWithTable, scout, Session, strings, TypeDescriptor, Widget
} from '../index';
import $ from 'jquery';
import 'jasmine-ajax';

let _jsonResourceCache = {};

/**
 * Utility functions for jasmine tests.
 */
export const JasmineScoutUtil = {

  /**
   * @returns the loaded JSON data structure
   */
  loadJsonResource(jsonResourceUrl: string, options: { useCache?: boolean } = {}): JQuery.Promise<any> {
    scout.assertParameter('jsonResourceUrl', jsonResourceUrl);

    if (scout.nvl(options.useCache, true)) {
      let json = _jsonResourceCache[jsonResourceUrl];
      if (json) {
        return $.resolvedPromise(json);
      }
    }

    return $.ajax({
      async: false,
      method: 'GET',
      dataType: 'json',
      contentType: 'application/json; charset=UTF-8',
      cache: false,
      url: jsonResourceUrl
    })
      .done(json => {
        if (scout.nvl(options.useCache, true)) {
          _jsonResourceCache[jsonResourceUrl] = json;
        }
        return $.resolvedPromise(json);
      })
      .fail((jqXHR, textStatus, errorThrown) => {
        throw new Error('Could not load resource from url: ' + jsonResourceUrl);
      });
  },

  loadJsonResourceAndMockRestCall(resourceUrlToMock: string, jsonResourceUrl: string, options: {
    useCache?: boolean;
    restriction?: any;
    method?: string;
  } = {}) {
    scout.assertParameter('resourceUrlToMock', resourceUrlToMock);

    JasmineScoutUtil.loadJsonResource(jsonResourceUrl, options)
      .then(json => JasmineScoutUtil.mockRestCall(resourceUrlToMock, json, options));
  },

  mockRestLookupCall(resourceUrlToMock: string, lookupRows: any[], parentRestriction?: any) {
    scout.assertParameter('resourceUrlToMock', resourceUrlToMock);

    // Normalize lookup rows
    lookupRows = arrays.ensure(lookupRows).map(lookupRow => $.extend({
      active: true,
      enabled: true,
      parentId: null
    }, lookupRow));

    // getAll()
    JasmineScoutUtil.mockRestCall(resourceUrlToMock, {
      rows: lookupRows
    }, {
      restriction: parentRestriction
    });

    // getKey()
    lookupRows.forEach(lookupRow => {
      JasmineScoutUtil.mockRestCall(resourceUrlToMock, {
        rows: [lookupRow]
      }, {
        restriction: lookupRow.id
      });
    });
  },

  mockRestCall(resourceUrlToMock: string, responseData: any, options: {
    restriction?: any;
    method?: string;
    /**
     * Used to serialize the responseData. Default is {@link JSON.stringify}.
     */
    stringify?: (any) => string;
  } = {}) {
    let url = new RegExp('.*' + strings.quote(resourceUrlToMock) + '.*');
    let data = options.restriction ? new RegExp('.*' + strings.quote(options.restriction) + '.*') : undefined;
    const stringify = options.stringify || JSON.stringify;
    const responseText = stringify(responseData);
    jasmine.Ajax.stubRequest(url, data, options.method).andReturn({
      status: 200,
      responseText
    });
  },

  mockDataObjectRestCall(resourceUrlToMock: string, responseData: BaseDoEntity | void, options: {
    restriction?: any;
    method?: string;
    /**
     * Used to serialize the responseData. Default is {@link dataObjects.stringify}.
     */
    stringify?: (any) => string;
  } = {}) {
    options.stringify = options.stringify || dataObjects.stringify;
    this.mockRestCall(resourceUrlToMock, responseData, options);
  },

  /**
   * If an ajax call is not mocked, this fallback will be triggered to show information about which url is not mocked.
   */
  captureNotMockedCalls() {
    jasmine.Ajax.stubRequest(/.*/).andCallFunction((request: JasmineAjaxRequest) => {
      fail('Ajax call not mocked for url: ' + request.url + ', method: ' + request.method);
    });
  },

  /**
   * Calls the given mock as soon as a hybrid action with the given actionType is called.
   * The mock is called asynchronously using setTimeout to let the runtime code add any required event listeners first.
   *
   * The mock may return an object with [id, widget] if the action is supposed to create widgets.
   * The format of the id depends on the method used to add widgets:
   * - `AbstractHybridAction.addWidget(IWidget)` (e.g. `AbstractFormHybridAction`): `${widgetId}`
   * - `AbstractHybridAction.addWidgets(Map<String, IWidget>)`: `${actionId}${widgetId}`
   */
  mockHybridAction<TData extends DoEntity>(session: Session, actionType: string, mock: (event: HybridActionEvent<TData>) => Record<string, Widget>) {
    let hm = HybridManager.get(session);
    hm.on('hybridAction', (event: HybridActionEvent<TData>) => {
      if (event.data.actionType === actionType) {
        setTimeout(() => {
          let widgets = mock(event);
          if (widgets) {
            hm.setProperty('widgets', widgets);
          }
        });
      }
    });
  },

  /**
   * Asserts that every page has an uuid and a specific {@link PageParamDo}, if required.
   */
  assertPageCompleteness(options?: PageCompletenessOptions) {
    options = options || {};
    let pagesNotRequiringUuid: Set<Constructor<Page> | AbstractConstructor<Page>> = new Set([PageWithNodes, PageWithTable, AutoLeafPageWithNodes, ...options.pagesNotRequiringUuid || []]);
    let pagesNotRequiringPageParam: Set<Constructor<Page> | AbstractConstructor<Page>> = new Set([PageWithNodes, PageWithTable, AutoLeafPageWithNodes, ...options.pagesNotRequiringPageParam || []]);
    let missingUuids: Set<string> = new Set();
    let missingPageParams = new Set();
    let completePages = new Set();

    for (const PageConstructor of ObjectFactory.get().getSubClassesOf(Page)) {
      let pageType = ObjectFactory.get().getObjectType(PageConstructor);
      if (options.namespace && !pageType.startsWith(options.namespace)) {
        continue;
      }

      let page = new PageConstructor();
      page.minimalInit();

      // Assert uuid
      if (scout.nvl(options.assertUuid, true) && !page.uuid && !pagesNotRequiringUuid.has(PageConstructor)) {
        missingUuids.add(pageType);
      }

      // Assert pageParam
      if (scout.nvl(options.assertPageParam, true)) {
        let pageParamType = `${pageType}ParamDo`;
        let PageParam = TypeDescriptor.resolveType(pageParamType);
        if (!PageParam && !pagesNotRequiringPageParam.has(PageConstructor)) {
          missingPageParams.add(pageType);
        }
      }

      if (!missingUuids.has(pageType) && !missingPageParams.has(pageType)) {
        completePages.add(pageType);
      }
    }

    if (missingUuids.size > 0) {
      fail([
        `Found ${missingUuids.size} pages without a uuid. Please ensure every page has a uuid.`,
        ...missingUuids
      ].join('\n'));
    }
    if (missingPageParams.size > 0) {
      fail([
        `Found ${missingPageParams.size} pages without a pageParam.`,
        'If a pageParam is required, create one and add `declare pageParam: NewPageParam` to your page.',
        'Otherwise, add the page to the ignore list (`options.pagesNotRequiringPageParam`).',
        ...missingPageParams
      ].join('\n'));
    }

    if (completePages.size > 0) {
      console.log(`PageCompleteness: the following pages are complete: ${Array.from(completePages).join(', ')}`);
    }

    if (completePages.size === 0 && missingUuids.size === 0 && missingPageParams.size === 0) {
      console.log('PageCompleteness: no pages found in this module.');
    }

    // A test without an expectation logs a warning
    expect(true).toBe(true);
  }
};

export type PageCompletenessOptions = {
  /**
   * Specifies whether the pages need an uuid. Default is true.
   */
  assertUuid?: boolean;
  /**
   * Specifies whether the pages need a pageParam. Default is true.
   */
  assertPageParam?: boolean;
  /**
   * Contains the pages that do not require an uuid.
   */
  pagesNotRequiringUuid?: (Constructor<Page> | AbstractConstructor<Page>)[];
  /**
   * Contains the pages that do not require a pageParam, e.g. because the page does not have any parameters.
   */
  pagesNotRequiringPageParam?: (Constructor<Page> | AbstractConstructor<Page>)[];
  /**
   * If specified, only the pages in this namespace are considered.
   */
  namespace?: string;
};
