import { Context } from '../log';
import {
  AdB2cService,
  ConflictError,
} from '../../gateways/adb2c/adb2c.service';
import { SendGridService } from '../../gateways/sendgrid/sendgrid.service';
import { User, newUser } from '../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
import { AccountsRepositoryService } from '../../repositories/accounts/accounts.repository.service';
import { Account } from '../../repositories/accounts/entity/account.entity';

// ### ユニットテスト用コード以外では絶対に使用してはいけないダーティな手段を使用しているが、他の箇所では使用しないこと ###

/**
 * adB2cServiceのモックを作成して、TServiceが依存するサービス(adB2cService)の参照を上書きする
 * ※ serviceに指定するオブジェクトは`adB2cService: AdB2cService`メンバ変数を持つ必要がある
 * @param service 上書きしたいTService
 * @param overrides adB2cServiceの各種メソッドのモックが返す値（省略した場合は既定のダミーの値）
 */
export const overrideAdB2cService = <TService>(
  service: TService,
  overrides: {
    createUser?: (
      context: Context,
      email: string,
      password: string,
      username: string,
    ) => Promise<{ sub: string } | ConflictError>;
    deleteUser?: (externalId: string, context: Context) => Promise<void>;
  },
): void => {
  // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
  const obj = (service as any).adB2cService as AdB2cService;
  if (overrides.createUser) {
    Object.defineProperty(obj, obj.createUser.name, {
      value: overrides.createUser,
      writable: true,
    });
  }
  if (overrides.deleteUser) {
    Object.defineProperty(obj, obj.deleteUser.name, {
      value: overrides.deleteUser,
      writable: true,
    });
  }
};

/**
 * sendgridServiceのモックを作成して、TServiceが依存するサービス(sendgridService)の参照を上書きする
 * ※ serviceに指定するオブジェクトは`sendgridService: SendgridService`メンバ変数を持つ必要がある
 * @param service 上書きしたいTService
 * @param overrides sendgridServiceの各種メソッドのモックが返す値（省略した場合は既定のダミーの値）
 */
export const overrideSendgridService = <TService>(
  service: TService,
  overrides: {
    createMailContentFromEmailConfirm?: (
      context: Context,
      accountId: number,
      userId: number,
      email: string,
    ) => Promise<{ subject: string; text: string; html: string }>;
    createMailContentFromEmailConfirmForNormalUser?: (
      accountId: number,
      userId: number,
      email: string,
    ) => Promise<{ subject: string; text: string; html: string }>;
    sendMail?: (
      context: Context,
      to: string,
      from: string,
      subject: string,
      text: string,
      html: string,
    ) => Promise<void>;
  },
): void => {
  // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
  const obj = (service as any).sendgridService as SendGridService;
  if (overrides.sendMail) {
    Object.defineProperty(obj, obj.sendMail.name, {
      value: overrides.sendMail,
      writable: true,
    });
  } else {
    // [重要]
    // sendMailだけは外部に対する送信を行ってしまう & 失敗によりメールアドレス自体の信頼度が変動してしまうため、
    // overrideした場合には"必ず"偽物の呼び出しになるようにしておく
    Object.defineProperty(obj, obj.sendMail.name, {
      value: async () => {
        return;
      },
      writable: true,
    });
  }

  if (overrides.createMailContentFromEmailConfirm) {
    Object.defineProperty(obj, obj.createMailContentFromEmailConfirm.name, {
      value: overrides.createMailContentFromEmailConfirm,
      writable: true,
    });
  }

  if (overrides.createMailContentFromEmailConfirmForNormalUser) {
    Object.defineProperty(
      obj,
      obj.createMailContentFromEmailConfirmForNormalUser.name,
      {
        value: overrides.createMailContentFromEmailConfirmForNormalUser,
        writable: true,
      },
    );
  }
};

/**
 * usersRepositoryのモックを作成して、TServiceが依存するサービス(usersRepositoryService)の参照を上書きする
 * ※ serviceに指定するオブジェクトは`usersRepository: UsersRepositoryService`メンバ変数を持つ必要がある
 * @param service 上書きしたいTService
 * @param overrides usersRepositoryの各種メソッドのモックが返す値（省略した場合は既定のダミーの値）
 */
export const overrideUsersRepositoryService = <TService>(
  service: TService,
  overrides: {
    createNormalUser?: (user: newUser) => Promise<User>;
    deleteNormalUser?: (userId: number) => Promise<void>;
  },
): void => {
  // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
  const obj = (service as any).usersRepository as UsersRepositoryService;
  if (overrides.createNormalUser) {
    Object.defineProperty(obj, obj.createNormalUser.name, {
      value: overrides.createNormalUser,
      writable: true,
    });
  }
  if (overrides.deleteNormalUser) {
    Object.defineProperty(obj, obj.deleteNormalUser.name, {
      value: overrides.deleteNormalUser,
      writable: true,
    });
  }
};

/**
 * blobStorageServiceのモックを作成して、TServiceが依存するサービス(blobStorageService)の参照を上書きする
 * ※ serviceに指定するオブジェクトは`blobstorageService: blobStorageService`メンバ変数を持つ必要がある
 * @param service 上書きしたいTService
 * @param overrides blobStorageServiceの各種メソッドのモックが返す値（省略した場合は既定のダミーの値）
 */
export const overrideBlobstorageService = <TService>(
  service: TService,
  overrides: {
    createContainer?: (
      context: Context,
      accountId: number,
      country: string,
    ) => Promise<void>;
    deleteContainer?: (
      context: Context,
      accountId: number,
      country: string,
    ) => Promise<void>;
  },
): void => {
  // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
  const obj = (service as any).blobStorageService as BlobstorageService;
  if (overrides.createContainer) {
    Object.defineProperty(obj, obj.createContainer.name, {
      value: overrides.createContainer,
      writable: true,
    });
  }
  if (overrides.deleteContainer) {
    Object.defineProperty(obj, obj.deleteContainer.name, {
      value: overrides.deleteContainer,
      writable: true,
    });
  }
};

/**
 * accountsRepositoryのモックを作成して、TServiceが依存するサービス(AccountsRepositoryService)の参照を上書きする
 * ※ serviceに指定するオブジェクトは`accountsRepository: AccountsRepositoryService`メンバ変数を持つ必要がある
 * @param service 上書きしたいTService
 * @param overrides accountsRepositoryの各種メソッドのモックが返す値（省略した場合は本物のメソッドが呼ばれる）
 */
export const overrideAccountsRepositoryService = <TService>(
  service: TService,
  overrides: {
    createAccount?: (
      companyName: string,
      country: string,
      dealerAccountId: number | undefined,
      tier: number,
      adminExternalUserId: string,
      adminUserRole: string,
      adminUserAcceptedTermsVersion: string,
    ) => Promise<{ newAccount: Account; adminUser: User }>;
    deleteAccount?: (accountId: number, userId: number) => Promise<void>;
  },
): void => {
  // テストコードでのみ許される強引な方法でprivateメンバ変数の参照を取得
  const obj = (service as any).accountRepository as AccountsRepositoryService;
  if (overrides.deleteAccount) {
    Object.defineProperty(obj, obj.deleteAccount.name, {
      value: overrides.deleteAccount,
      writable: true,
    });
  }
  if (overrides.createAccount) {
    Object.defineProperty(obj, obj.createAccount.name, {
      value: overrides.createAccount,
      writable: true,
    });
  }
};
