import { v4 as uuidv4 } from 'uuid';
import { DataSource } from 'typeorm';
import { User, UserArchive } from '../../repositories/users/entity/user.entity';
import { Account } from '../../repositories/accounts/entity/account.entity';
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
import { License } from '../../repositories/licenses/entity/license.entity';

type InitialTestDBState = {
  tier1Accounts: { account: Account; users: User[] }[];
  tier2Accounts: { account: Account; users: User[] }[];
  tier3Accounts: { account: Account; users: User[] }[];
  tier4Accounts: { account: Account; users: User[] }[];
  tier5Accounts: { account: Account; users: User[] }[];
};

// 上書きされたら困る項目を除外したAccount型
type OverrideAccount = Omit<
  Account,
  'id' | 'primary_admin_user_id' | 'secondary_admin_user_id' | 'user'
>;

// 上書きされたら困る項目を除外したUser型
type OverrideUser = Omit<
  User,
  'id' | 'account' | 'license' | 'userGroupMembers'
>;

type AccountDefault = { [K in keyof OverrideAccount]?: OverrideAccount[K] };
type UserDefault = { [K in keyof OverrideUser]?: OverrideUser[K] };

/**
 * テスト ユーティリティ: 1~4階層のアカウントとその管理者ユーザーを作成します
 * @param dataSource データソース
 * @returns 作成されたデータセット
 */
export const makeHierarchicalAccounts = async (
  datasource: DataSource,
): Promise<InitialTestDBState> => {
  const state: InitialTestDBState = {
    tier1Accounts: [],
    tier2Accounts: [],
    tier3Accounts: [],
    tier4Accounts: [],
    tier5Accounts: [],
  };

  // 第1階層を作成
  {
    const { account, admin } = await makeTestAccount(datasource, {
      tier: 1,
      company_name: 'OMDS',
    });

    state.tier1Accounts.push({
      account: account,
      users: [admin],
    });
  }
  // 第2階層を作成
  {
    const tier1 = state.tier1Accounts.slice().shift();
    {
      const { account, admin } = await makeTestAccount(datasource, {
        tier: 2,
        parent_account_id: tier1?.account.id,
        company_name: 'OMDS_US',
      });
      state.tier2Accounts.push({
        account: account,
        users: [admin],
      });
    }
    {
      const { account, admin } = await makeTestAccount(datasource, {
        tier: 2,
        parent_account_id: tier1?.account.id,
        company_name: 'OMDS_EU',
      });
      state.tier2Accounts.push({
        account: account,
        users: [admin],
      });
    }
  }

  // 第3階層を作成
  {
    for (const v of state.tier2Accounts) {
      {
        const { account, admin } = await makeTestAccount(datasource, {
          tier: 3,
          parent_account_id: v.account.id,
          company_name: `Agency_${v.account.id}_01`,
        });
        state.tier3Accounts.push({
          account: account,
          users: [admin],
        });
      }
      {
        const { account, admin } = await makeTestAccount(datasource, {
          tier: 3,
          parent_account_id: v.account.id,
          company_name: `Agency_${v.account.id}_02`,
        });
        state.tier3Accounts.push({
          account: account,
          users: [admin],
        });
      }
    }
    // 第4階層を作成
    for (const v of state.tier3Accounts) {
      {
        const { account, admin } = await makeTestAccount(datasource, {
          tier: 4,
          parent_account_id: v.account.id,
          company_name: `Distributor_${v.account.id}_01`,
        });
        state.tier4Accounts.push({
          account: account,
          users: [admin],
        });
      }
      {
        const { account, admin } = await makeTestAccount(datasource, {
          tier: 4,
          parent_account_id: v.account.id,
          company_name: `Distributor_${v.account.id}_02`,
        });
        state.tier4Accounts.push({
          account: account,
          users: [admin],
        });
      }
    }
  }

  return state;
};

/**
 * テスト ユーティリティ: 指定したプロパティを上書きしたアカウントとその管理者ユーザーを作成する
 * @param dataSource データソース
 * @param defaultUserValue Account型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
 * @param defaultAdminUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト(account_id等の所属関係が破壊される上書きは無視する)
 * @returns 作成したアカウント
 */
export const makeTestAccount = async (
  datasource: DataSource,
  defaultAccountValue?: AccountDefault,
  defaultAdminUserValue?: UserDefault,
  isPrimaryAdminNotExist?: boolean,
  isSecondaryAdminNotExist?: boolean,
): Promise<{ account: Account; admin: User }> => {
  let accountId: number;
  let userId: number;
  {
    const d = defaultAccountValue;
    const { identifiers } = await datasource.getRepository(Account).insert({
      tier: d?.tier ?? 1,
      parent_account_id: d?.parent_account_id ?? undefined,
      country: d?.country ?? 'US',
      delegation_permission: d?.delegation_permission ?? false,
      locked: d?.locked ?? false,
      company_name: d?.company_name ?? 'test inc.',
      verified: d?.verified ?? true,
      deleted_at: d?.deleted_at ?? '',
      created_by: d?.created_by ?? 'test_runner',
      created_at: d?.created_at ?? new Date(),
      updated_by: d?.updated_by ?? 'updater',
      updated_at: d?.updated_at ?? new Date(),
    });
    const result = identifiers.pop() as Account;
    accountId = result.id;
  }
  {
    const d = defaultAdminUserValue;
    const { identifiers } = await datasource.getRepository(User).insert({
      external_id: d?.external_id ?? uuidv4(),
      account_id: accountId,
      role: d?.role ?? 'admin none',
      author_id: d?.author_id ?? undefined,
      accepted_eula_version: d?.accepted_eula_version ?? '1.0',
      accepted_privacy_notice_version:
        d?.accepted_privacy_notice_version ?? '1.0',
      accepted_dpa_version: d?.accepted_dpa_version ?? '1.0',
      email_verified: d?.email_verified ?? true,
      auto_renew: d?.auto_renew ?? true,
      notification: d?.notification ?? true,
      encryption: d?.encryption ?? true,
      encryption_password: d?.encryption_password ?? 'password',
      prompt: d?.prompt ?? true,
      deleted_at: d?.deleted_at ?? '',
      created_by: d?.created_by ?? 'test_runner',
      created_at: d?.created_at ?? new Date(),
      updated_by: d?.updated_by ?? 'updater',
      updated_at: d?.updated_at ?? new Date(),
    });

    const result = identifiers.pop() as User;
    userId = result.id;
  }

  // Accountの管理者を設定する
  let secondaryAdminUserId: number | null = null;
  if (isPrimaryAdminNotExist && !isSecondaryAdminNotExist) {
    secondaryAdminUserId = userId;
  }
  await datasource.getRepository(Account).update(
    { id: accountId },
    {
      primary_admin_user_id: isPrimaryAdminNotExist ? null : userId,
      secondary_admin_user_id: secondaryAdminUserId,
    },
  );

  const account = await datasource.getRepository(Account).findOne({
    where: {
      id: accountId,
    },
  });

  const admin = await datasource.getRepository(User).findOne({
    where: {
      id: userId,
    },
  });
  if (!account || !admin) {
    throw new Error('Unexpected null');
  }

  return {
    account: account,
    admin: admin,
  };
};

/**
 * テスト ユーティリティ: 指定したプロパティを上書きした管理者ユーザーの存在しないアカウントを作成する
 * @param dataSource データソース
 * @param defaultUserValue Account型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
 * @returns 作成したアカウント
 */
export const makeTestSimpleAccount = async (
  datasource: DataSource,
  defaultAccountValue?: AccountDefault,
): Promise<Account> => {
  const d = defaultAccountValue;
  const { identifiers } = await datasource.getRepository(Account).insert({
    tier: d?.tier ?? 1,
    parent_account_id: d?.parent_account_id ?? undefined,
    country: d?.country ?? 'US',
    delegation_permission: d?.delegation_permission ?? false,
    locked: d?.locked ?? false,
    company_name: d?.company_name ?? 'test inc.',
    verified: d?.verified ?? true,
    deleted_at: d?.deleted_at ?? '',
    created_by: d?.created_by ?? 'test_runner',
    created_at: d?.created_at ?? new Date(),
    updated_by: d?.updated_by ?? 'updater',
    updated_at: d?.updated_at ?? new Date(),
  });
  const result = identifiers.pop() as Account;

  const account = await datasource.getRepository(Account).findOne({
    where: {
      id: result.id,
    },
  });
  if (!account) {
    throw new Error('Unexpected null');
  }
  return account;
};

/**
 * テスト ユーティリティ: 指定したプロパティを上書きしたユーザーを作成する
 * @param dataSource データソース
 * @param defaultUserValue User型と同等かつoptionalなプロパティを持つ上書き箇所指定用オブジェクト
 * @returns 作成したユーザー
 */
export const makeTestUser = async (
  datasource: DataSource,
  defaultUserValue?: UserDefault,
): Promise<User> => {
  const d = defaultUserValue;
  const { identifiers } = await datasource.getRepository(User).insert({
    account_id: d?.account_id ?? -1,
    external_id: d?.external_id ?? uuidv4(),
    role: d?.role ?? `${ADMIN_ROLES.STANDARD} ${USER_ROLES.NONE}`,
    author_id: d?.author_id,
    accepted_eula_version: d?.accepted_eula_version ?? '1.0',
    accepted_dpa_version: d?.accepted_dpa_version ?? '1.0',
    email_verified: d?.email_verified ?? true,
    auto_renew: d?.auto_renew ?? true,
    notification: d?.notification ?? true,
    encryption: d?.encryption ?? true,
    encryption_password: d?.encryption_password,
    prompt: d?.prompt ?? true,
    created_by: d?.created_by ?? 'test_runner',
    created_at: d?.created_at ?? new Date(),
    updated_by: d?.updated_by ?? 'updater',
    updated_at: d?.updated_at ?? new Date(),
  });
  const result = identifiers.pop() as User;

  const user = await datasource.getRepository(User).findOne({
    where: {
      id: result.id,
    },
  });
  if (!user) {
    throw new Error('Unexpected null');
  }
  return user;
};

/**
 * テスト ユーティリティ: 指定IDのアカウントを取得する
 * @param dataSource データソース
 * @param id アカウントID
 * @returns 該当アカウント
 */
export const getAccount = async (
  dataSource: DataSource,
  id: number,
): Promise<Account | null> => {
  return await dataSource.getRepository(Account).findOne({
    where: { id: id },
  });
};

/**
 * テスト ユーティリティ: すべてのアカウントを取得する
 * @param dataSource データソース
 * @returns　該当アカウント一覧
 */
export const getAccounts = async (
  dataSource: DataSource,
): Promise<Account[]> => {
  return await dataSource.getRepository(Account).find();
};

/**
 * テスト ユーティリティ: 指定ExternalIdのユーザーを取得する
 * @param dataSource データソース
 * @param externalId 外部ID
 * @returns 該当ユーザー
 */
export const getUserFromExternalId = async (
  dataSource: DataSource,
  externalId: string,
) => {
  return await dataSource.getRepository(User).findOne({
    where: { external_id: externalId },
  });
};

/**
 * テスト ユーティリティ: 指定Idのユーザーを取得する
 * @param dataSource データソース
 * @param externalId 外部ID
 * @returns 該当ユーザー
 */
export const getUser = async (
  datasource: DataSource,
  id: number,
): Promise<User | null> => {
  const user = await datasource.getRepository(User).findOne({
    where: {
      id: id,
    },
  });
  return user;
};

/**
 * テスト ユーティリティ: すべてのユーザーを取得する
 * @param dataSource データソース
 * @returns 該当ユーザー一覧
 */
export const getUsers = async (dataSource: DataSource): Promise<User[]> => {
  return await dataSource.getRepository(User).find();
};

/**
 * テスト ユーティリティ: ユーザー退避テーブルの内容を取得する
 * @param dataSource データソース
 * @returns ユーザー退避テーブルの内容
 */
export const getUserArchive = async (
  dataSource: DataSource,
): Promise<UserArchive[]> => {
  return await dataSource.getRepository(UserArchive).find();
};
export const getLicenses = async (
  datasource: DataSource,
  account_id: number,
): Promise<License[]> => {
  const licenses = await datasource.getRepository(License).find({
    where: {
      account_id: account_id,
    },
  });
  return licenses;
};
