/*
 This file is part of GNU Taler
 (C) 2022 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

import {
  NotificationType,
  TalerBankIntegrationHttpClient,
  TalerCoreBankHttpClient,
  TalerRevenueHttpClient,
  TalerWireGatewayHttpClient,
  WalletNotification,
} from "@gnu-taler/taler-util";
import {
  WalletCoreApiClient,
  WalletCoreOpKeys,
  WalletCoreRequestType,
  WalletCoreResponseType,
} from "@gnu-taler/taler-wallet-core";
import {
  ApiContextProvider,
  TranslationProvider,
  defaultRequestHandler,
} from "@gnu-taler/web-util/browser";
import {
  ComponentChildren,
  FunctionalComponent,
  VNode,
  h as create,
} from "preact";
import { AlertProvider } from "./context/alert.js";
import { BackendProvider } from "./context/backend.js";
import { strings } from "./i18n/strings.js";
import { nullFunction } from "./mui/handlers.js";
import { BackgroundApiClient, wxApi } from "./wxApi.js";

// export const nullFunction: any = () => null;

interface MockHandler {
  addWalletCallResponse<Op extends WalletCoreOpKeys>(
    operation: Op,
    payload?: Partial<WalletCoreRequestType<Op>>,
    response?: WalletCoreResponseType<Op>,
    callback?: () => void,
  ): MockHandler;

  getCallingQueueState(): "empty" | string;

  notifyEventFromWallet(notif: WalletNotification): void;
}

type CallRecord = WalletCallRecord | BackgroundCallRecord;
interface WalletCallRecord {
  source: "wallet";
  callback: () => void;
  operation: WalletCoreOpKeys;
  payload?: WalletCoreRequestType<WalletCoreOpKeys>;
  response?: WalletCoreResponseType<WalletCoreOpKeys>;
}
interface BackgroundCallRecord {
  source: "background";
  name: string;
  args: any;
  response: any;
}

type Subscriptions = {
  [key in NotificationType]?: (d: WalletNotification) => void;
};

export function createWalletApiMock(): {
  handler: MockHandler;
  TestingContext: FunctionalComponent<{ children: ComponentChildren }>;
} {
  const calls = new Array<CallRecord>();
  const subscriptions: Subscriptions = {};

  const mock: typeof wxApi = {
    wallet: new Proxy<WalletCoreApiClient>({} as any, {
      get(target, name, receiver) {
        const functionName = String(name);
        if (functionName !== "call") {
          throw Error(
            `the only method in wallet api should be 'call': ${functionName}`,
          );
        }
        return function (
          operation: WalletCoreOpKeys,
          payload: WalletCoreRequestType<WalletCoreOpKeys>,
        ) {
          const next = calls.shift();

          if (!next) {
            throw Error(
              `wallet operation was called but none was expected: ${operation} (${JSON.stringify(
                payload,
                undefined,
                2,
              )})`,
            );
          }
          if (next.source !== "wallet") {
            throw Error(`wallet operation expected`);
          }
          if (operation !== next.operation) {
            //more checks, deep check payload
            throw Error(
              `wallet operation doesn't match: expected ${next.operation} actual ${operation}`,
            );
          }
          next.callback();

          return next.response ?? {};
        };
      },
    }),
    listener: {
      trigger: () => {},
      onUpdateNotification(
        mTypes: NotificationType[],
        callback: ((d: WalletNotification) => void) | undefined,
      ): () => void {
        mTypes.forEach((m) => {
          subscriptions[m] = callback;
        });
        return nullFunction;
      },
    },
    background: new Proxy<BackgroundApiClient>({} as any, {
      get(target, name, receiver) {
        const functionName = String(name);
        return function (...args: any) {
          const next = calls.shift();
          if (!next) {
            throw Error(
              `background operation was called but none was expected: ${functionName} (${JSON.stringify(
                args,
                undefined,
                2,
              )})`,
            );
          }
          if (next.source !== "background" || functionName !== next.name) {
            //more checks, deep check args
            throw Error(`background operation doesn't match`);
          }
          return next.response;
        };
      },
    }),
  };

  const handler: MockHandler = {
    addWalletCallResponse(operation, payload, response, cb) {
      calls.push({
        source: "wallet",
        operation,
        payload,
        response,
        callback: cb
          ? cb
          : () => {
              null;
            },
      });
      return handler;
    },
    notifyEventFromWallet(event: WalletNotification): void {
      const callback = subscriptions[event.type];
      if (!callback)
        throw Error(`Expected to have a subscription for ${event}`);
      return callback(event);
    },
    getCallingQueueState() {
      return calls.length === 0 ? "empty" : `${calls.length} left`;
    },
  };

  function TestingContext({
    children: _cs,
  }: {
    children: ComponentChildren;
  }): VNode {
    let children = _cs;
    children = create(AlertProvider, { children }, children);
    const value = {
      request: defaultRequestHandler,
      bankCore: new TalerCoreBankHttpClient("/"),
      bankIntegration: new TalerBankIntegrationHttpClient("/"),
      bankWire: new TalerWireGatewayHttpClient("/"),
      bankRevenue: new TalerRevenueHttpClient("/"),
    };
    children = create(ApiContextProvider, { value, children }, children);
    children = create(
      TranslationProvider,
      { children, source: strings, initial: "en", forceLang: "en" },
      children,
    );
    return create(
      BackendProvider,
      {
        wallet: mock.wallet,
        background: mock.background,
        listener: mock.listener,
        children,
      },
      children,
    );
  }

  return { handler, TestingContext };
}
