import { ConfigModule, ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { Aadb2cUser, B2cMetadata, JwkSignKey } from '../../../common/token';
import {
  AdB2cService,
  ConflictError,
} from '../../../gateways/adb2c/adb2c.service';
import { SendGridService } from '../../../gateways/sendgrid/sendgrid.service';
import { User } from '../../../repositories/users/entity/user.entity';
import { UsersRepositoryService } from '../../../repositories/users/users.repository.service';
import { UsersService } from '../users.service';
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
import { SortCriteriaRepositoryService } from '../../../repositories/sort_criteria/sort_criteria.repository.service';
import {
  SortDirection,
  TaskListSortableAttribute,
} from '../../../common/types/sort';
import { AdB2cUser } from '../../../gateways/adb2c/types/types';

export type SortCriteriaRepositoryMockValue = {
  updateSortCriteria: SortCriteria | Error;
  getSortCriteria: SortCriteria | Error;
};

export type UsersRepositoryMockValue = {
  updateUserVerified: undefined | Error;
  findUserById: User | Error;
  createNormalUser: User | Error;
  findSameAccountUsers: User[] | Error;
  findUserByExternalId: User | Error;
  existsAuthorId: boolean | Error;
};

export type AdB2cMockValue = {
  getMetaData: B2cMetadata | Error;
  getSignKeySets: JwkSignKey[] | Error;
  changePassword: { sub: string } | Error;
  createUser: string | ConflictError | Error;
  getUser: Aadb2cUser | Error;
  getUsers: AdB2cUser[] | Error;
};

export type SendGridMockValue = {
  createMailContentFromEmailConfirm: {
    subject: string;
    text: string;
    html: string;
  };
  createMailContentFromEmailConfirmForNormalUser:
    | { subject: string; text: string; html: string }
    | Error;
  sendMail: undefined | Error;
};

export type ConfigMockValue = {
  get: string | Error;
};

export const makeUsersServiceMock = async (
  usersRepositoryMockValue: UsersRepositoryMockValue,
  adB2cMockValue: AdB2cMockValue,
  sendGridMockValue: SendGridMockValue,
  configMockValue: ConfigMockValue,
  sortCriteriaRepositoryMockValue: SortCriteriaRepositoryMockValue,
): Promise<UsersService> => {
  const module: TestingModule = await Test.createTestingModule({
    providers: [UsersService],
    imports: [
      ConfigModule.forRoot({
        envFilePath: ['.env.local', '.env'],
      }),
    ],
  })
    .useMocker((token) => {
      switch (token) {
        case UsersRepositoryService:
          return makeUsersRepositoryMock(usersRepositoryMockValue);
        case AdB2cService:
          return makeAdB2cServiceMock(adB2cMockValue);
        case SendGridService:
          return makeSendGridMock(sendGridMockValue);
        case ConfigService:
          return makeConfigMock(configMockValue);
        case SortCriteriaRepositoryService:
          return makeSortCriteriaRepositoryMock(
            sortCriteriaRepositoryMockValue,
          );
      }
    })
    .compile();

  return module.get<UsersService>(UsersService);
};

export const makeSortCriteriaRepositoryMock = (
  value: SortCriteriaRepositoryMockValue,
) => {
  const { updateSortCriteria, getSortCriteria } = value;

  return {
    updateSortCriteria:
      updateSortCriteria instanceof Error
        ? jest
            .fn<
              Promise<void>,
              [number, TaskListSortableAttribute, SortDirection]
            >()
            .mockRejectedValue(updateSortCriteria)
        : jest
            .fn<
              Promise<SortCriteria>,
              [number, TaskListSortableAttribute, SortDirection]
            >()
            .mockResolvedValue(updateSortCriteria),
    getSortCriteria:
      getSortCriteria instanceof Error
        ? jest.fn<Promise<void>, [number]>().mockRejectedValue(getSortCriteria)
        : jest
            .fn<Promise<SortCriteria>, [number]>()
            .mockResolvedValue(getSortCriteria),
  };
};

export const makeSendGridServiceMock = (value: SendGridMockValue) => {
  const { createMailContentFromEmailConfirm, sendMail } = value;
  return {
    createMailContentFromEmailConfirm:
      createMailContentFromEmailConfirm instanceof Error
        ? jest
            .fn<Promise<void>, []>()
            .mockRejectedValue(createMailContentFromEmailConfirm)
        : jest
            .fn<Promise<{ subject: string; text: string; html: string }>, []>()
            .mockResolvedValue(createMailContentFromEmailConfirm),
    sendMail:
      sendMail instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(sendMail)
        : jest.fn<Promise<void>, []>().mockResolvedValue(sendMail),
  };
};

export const makeAdB2cServiceMock = (value: AdB2cMockValue) => {
  const {
    getMetaData,
    getSignKeySets,
    changePassword,
    createUser,
    getUser,
    getUsers,
  } = value;

  return {
    getMetaData:
      getMetaData instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(getMetaData)
        : jest.fn<Promise<B2cMetadata>, []>().mockResolvedValue(getMetaData),
    getSignKeySets:
      getSignKeySets instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(getSignKeySets)
        : jest
            .fn<Promise<JwkSignKey[]>, []>()
            .mockResolvedValue(getSignKeySets),
    changePassword:
      changePassword instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(changePassword)
        : jest
            .fn<Promise<{ sub: string }>, []>()
            .mockResolvedValue(changePassword),
    createUser:
      createUser instanceof Error
        ? jest.fn<Promise<ConflictError>, []>().mockRejectedValue(createUser)
        : jest
            .fn<Promise<string | ConflictError>, []>()
            .mockResolvedValue(createUser),
    getUser:
      getUser instanceof Error
        ? jest
            .fn<Promise<Aadb2cUser | undefined>, []>()
            .mockRejectedValue(getUser)
        : jest
            .fn<Promise<Aadb2cUser | undefined>, []>()
            .mockResolvedValue(getUser),
    getUsers:
      getUsers instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(getUsers)
        : jest.fn<Promise<AdB2cUser[]>, []>().mockResolvedValue(getUsers),
  };
};

class authorIdError extends Error {
  constructor(public code: string, e?: string) {
    super(e);
  }
}

export const makeUsersRepositoryMock = (value: UsersRepositoryMockValue) => {
  const {
    updateUserVerified,
    findUserById,
    createNormalUser,
    findSameAccountUsers,
    findUserByExternalId,
    existsAuthorId,
  } = value;

  const aIdError = new authorIdError('ER_DUP_ENTRY');

  return {
    updateUserVerified:
      updateUserVerified instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(updateUserVerified)
        : jest.fn<Promise<void>, []>().mockResolvedValue(updateUserVerified),
    findUserById:
      findUserById instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(findUserById)
        : jest.fn<Promise<User>, []>().mockResolvedValue(findUserById),
    createNormalUser:
      createNormalUser instanceof Error
        ? createNormalUser.name == 'ER_DUP_ENTRY'
          ? jest.fn<Promise<void>, []>().mockRejectedValue(aIdError)
          : jest.fn<Promise<void>, []>().mockRejectedValue(createNormalUser)
        : jest.fn<Promise<User>, []>().mockResolvedValue(createNormalUser),
    findSameAccountUsers:
      findSameAccountUsers instanceof Error
        ? jest
            .fn<Promise<User[] | undefined>, []>()
            .mockRejectedValue(findSameAccountUsers)
        : jest
            .fn<Promise<User[] | undefined>, []>()
            .mockResolvedValue(findSameAccountUsers),
    findUserByExternalId:
      findUserByExternalId instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(findUserByExternalId)
        : jest.fn<Promise<User>, []>().mockResolvedValue(findUserByExternalId),
    existsAuthorId:
      existsAuthorId instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(existsAuthorId)
        : jest.fn<Promise<boolean>, []>().mockResolvedValue(existsAuthorId),
  };
};

export const makeSendGridMock = (value: SendGridMockValue) => {
  const { sendMail, createMailContentFromEmailConfirmForNormalUser } = value;

  return {
    sendMail:
      sendMail instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(sendMail)
        : jest.fn<Promise<void>, []>().mockResolvedValue(sendMail),
    createMailContentFromEmailConfirmForNormalUser:
      createMailContentFromEmailConfirmForNormalUser instanceof Error
        ? jest
            .fn<Promise<void>, []>()
            .mockRejectedValue(createMailContentFromEmailConfirmForNormalUser)
        : jest
            .fn<Promise<{ subject: string; text: string; html: string }>, []>()
            .mockResolvedValue(createMailContentFromEmailConfirmForNormalUser),
  };
};

export const makeConfigMock = (value: ConfigMockValue) => {
  const { get } = value;

  return {
    get:
      get instanceof Error
        ? jest.fn<Promise<void>, []>().mockRejectedValue(get)
        : jest.fn<Promise<string>, []>().mockResolvedValue(get),
  };
};

export const makeDefaultSendGridlValue = (): SendGridMockValue => {
  return {
    sendMail: undefined,
    createMailContentFromEmailConfirm: { subject: '', text: '', html: '' },
    createMailContentFromEmailConfirmForNormalUser: {
      subject: 'test',
      text: 'test',
      html: 'test',
    },
  };
};

export const makeDefaultConfigValue = (): ConfigMockValue => {
  return {
    get: `test@example.co.jp`,
  };
};

export const makeDefaultSortCriteriaRepositoryMockValue =
  (): SortCriteriaRepositoryMockValue => {
    const sortCriteria = new SortCriteria();
    {
      sortCriteria.id = 1;
      sortCriteria.direction = 'ASC';
      sortCriteria.parameter = 'JOB_NUMBER';
      sortCriteria.user_id = 1;
    }
    return {
      updateSortCriteria: sortCriteria,
      getSortCriteria: sortCriteria,
    };
  };

export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => {
  return {
    getMetaData: {
      issuer: 'issuer',
    },
    getSignKeySets: [
      {
        kid: 'kid',
        nbf: 1111111111,
        use: 'sig',
        kty: 'RSA',
        e: 'e',
        n: 'n',
      },
    ],
    changePassword: {
      sub: 'TEST9999',
    },
    createUser: '001',
    getUser: {
      displayName: 'Hanako Sato',
      mail: 'hanako@sample.com',
    },
    getUsers: AdB2cMockUsers,
  };
};

// 個別のテストケースに対応してそれぞれのMockを用意するのは無駄が多いのでテストケース内で個別の値を設定する
export const makeDefaultUsersRepositoryMockValue =
  (): UsersRepositoryMockValue => {
    const newUser = new User();
    newUser.id = 111;

    const user1 = new User();
    user1.id = 2;
    user1.external_id = 'ede66c43-9b9d-4222-93ed-5f11c96e08e2';
    user1.account_id = 1234567890123456;
    user1.role = 'none';
    user1.author_id = '6cce347f-0cf1-a15e-19ab-d00988b643f9';
    user1.accepted_terms_version = '1.0';
    user1.email_verified = true;
    user1.auto_renew = false;
    user1.license_alert = false;
    user1.notification = false;
    user1.encryption = false;
    user1.prompt = false;
    user1.deleted_at = null;
    user1.created_by = 'test';
    user1.created_at = new Date();
    user1.updated_by = null;
    user1.updated_at = null;

    const user2 = new User();
    user2.id = 3;
    user2.external_id = '698176dc-4028-4638-a452-f00bf62a7781';
    user2.account_id = 1234567890123456;
    user2.role = 'none';
    user2.author_id = '551c4077-5b55-a38c-2c55-cd1edd537aa8';
    user2.accepted_terms_version = '1.0';
    user2.email_verified = true;
    user2.auto_renew = false;
    user2.license_alert = false;
    user2.notification = false;
    user2.encryption = false;
    user2.prompt = false;
    user2.deleted_at = null;
    user2.created_by = 'test';
    user2.created_at = new Date();
    user2.updated_by = null;
    user2.updated_at = null;

    return {
      updateUserVerified: undefined,
      findUserById: newUser,
      createNormalUser: newUser,
      findSameAccountUsers: [user1, user2],
      findUserByExternalId: newUser,
      existsAuthorId: false,
    };
  };

const AdB2cMockUsers: AdB2cUser[] = [
  {
    id: 'external_id1',
    displayName: 'test1',
    identities: [
      {
        signInType: 'emailAddress',
        issuer: 'issuer',
        issuerAssignedId: 'test1@mail.com',
      },
    ],
  },
  {
    id: 'external_id2',
    displayName: 'test2',
    identities: [
      {
        signInType: 'emailAddress',
        issuer: 'issuer',
        issuerAssignedId: 'test2@mail.com',
      },
    ],
  },
  {
    id: 'external_id3',
    displayName: 'test3',
    identities: [
      {
        signInType: 'emailAddress',
        issuer: 'issuer',
        issuerAssignedId: 'test3@mail.com',
      },
    ],
  },
];
