import { Chance } from 'chance';
import { whenRequest } from '@wix/fe-essentials/http-client/testkit/client';
import {
  listTags,
  queryTags,
  queryTagsForAccount,
  createTag,
  updateTag,
  deleteTag,
  createTagForAccount,
  updateTagForAccount,
} from '@wix/bex-utils/@wix/ambassador-os-tags-v1-tag/http';
import {
  QueryTagsRequest,
  Tag as TagType,
} from '@wix/bex-utils/@wix/ambassador-os-tags-v1-tag/types';
import { InMemoryBackend } from '../testkit/backend';
import { createHttpErrorMock } from './HttpErrorMocks';
import { ExpectedTestError } from '../__tests__/utils/ExpectedTestError';

class ServerError extends ExpectedTestError {
  readonly code: string;

  constructor(code: string, message: string) {
    super(message);
    this.code = code;
  }
}

const chance = new Chance();

const aTag = (overrides: Partial<TagType> = {}): TagType => {
  return {
    id: chance.guid(),
    name: chance.word({ length: 5 }),
    createdDate: chance.date({
      min: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
      max: new Date(),
    }) as Date,
    ...overrides,
  } as TagType;
};

export const aTagsArray = (count: number) =>
  Array.from({ length: count }, () => aTag());

export const generateInvalidLongName = (length: number) =>
  chance.string({ length });
export const TAGS = aTagsArray(2);

const buildQueryTagsReply =
  (tags?: TagType[]) =>
  async ({ query }: Partial<QueryTagsRequest>) => {
    const allTags = tags || TAGS;
    const search = query?.filter?.name?.$startsWith as string | undefined;
    const filteredTags = search
      ? allTags.filter((tag) =>
          tag.name?.toLowerCase().startsWith(search.toLowerCase()),
        )
      : allTags;
    const limit = query?.cursorPaging?.limit ?? filteredTags.length;
    const cursorToken = query?.cursorPaging?.cursor;
    const offset = cursorToken ? parseInt(atob(cursorToken), 10) : 0;
    const pageItems = filteredTags.slice(offset, offset + limit);
    const nextOffset = offset + limit;
    const hasNext = nextOffset < filteredTags.length;
    return {
      tags: pageItems,
      pagingMetadata: {
        count: pageItems.length,
        cursors: { next: hasNext ? btoa(String(nextOffset)) : null },
        hasNext,
        total: filteredTags.length,
      },
    };
  };

export const withQueryTags = (tags?: TagType[]) =>
  whenRequest(queryTags).reply(200, buildQueryTagsReply(tags)).persist();

export const withQueryTagsForAccount = (tags?: TagType[]) =>
  whenRequest(queryTagsForAccount)
    .reply(200, buildQueryTagsReply(tags))
    .persist();

export const withListTags = (tags?: TagType[]) => withQueryTags(tags);

export const withListTagsForAccount = (tags?: TagType[]) =>
  withQueryTagsForAccount(tags);

const createTagResponseMock = (tag: TagType) => ({
  __applicationErrorsType: {},
  tag: { createdDate: new Date(), updatedDate: new Date(), ...tag },
});

export const withCreateTag = (mock?: jest.Mock<any, any>) =>
  whenRequest(createTag)
    .reply(
      200,
      mock ||
        (async ({ tag }) => {
          return createTagResponseMock(tag);
        }),
    )
    .persist();

export const withCreateTagForAccount = (mock?: jest.Mock<any, any>) =>
  whenRequest(createTagForAccount)
    .reply(
      200,
      mock ||
        (async ({ tag }) => {
          return createTagResponseMock(tag);
        }),
    )
    .persist();

export const withUpdateTag = () =>
  whenRequest(updateTag)
    .reply(200, async (tag) => {
      return tag;
    })
    .persist();

export const withUpdateTagForAccount = () =>
  whenRequest(updateTagForAccount)
    .reply(200, async (tag) => {
      return tag;
    })
    .persist();

export const withDeleteTag = () =>
  whenRequest(deleteTag)
    .reply(200, async () => {
      return {};
    })
    .persist();

export const getQueryTagsErrorMock = ({
  code = 'ERR_NETWORK',
  status = 429,
}: {
  code?: string;
  status?: number;
} = {}) =>
  whenRequest(queryTags)
    .reply(200, async () => {
      return new Promise((_resolve, reject) => {
        window.setTimeout(() => {
          reject(createHttpErrorMock({ code, status }));
        }, 500);
      });
    })
    .persist();

export const getListTagsErrorMock = ({
  code = 'ERR_NETWORK',
  status = 429,
}: {
  code?: string;
  status?: number;
} = {}) => getQueryTagsErrorMock({ code, status });

export const getListTagsForAccountErrorMock = ({
  code = 'ERR_NETWORK',
  status = 429,
}: {
  code?: string;
  status?: number;
} = {}) =>
  whenRequest(queryTagsForAccount)
    .reply(200, async () => {
      return new Promise((_resolve, reject) => {
        setTimeout(() => {
          reject(createHttpErrorMock({ code, status }));
        }, 500);
      });
    })
    .persist();

export const getCreateTagErrorMock = ({
  code = 'ERR_NETWORK',
  status = 429,
  requestId,
}: {
  code?: string;
  status?: number;
  requestId?: string;
} = {}) =>
  whenRequest(createTag)
    .reply(200, async () => {
      return new Promise((_resolve, reject) => {
        window.setTimeout(() => {
          reject(createHttpErrorMock({ code, status, requestId }));
        }, 500);
      });
    })
    .persist();

export const getCreateTagForAccountErrorMock = ({
  code = 'ERR_NETWORK',
  status = 429,
}: {
  code?: string;
  status?: number;
} = {}) =>
  whenRequest(createTagForAccount)
    .reply(200, async () => {
      return new Promise((_resolve, reject) => {
        window.setTimeout(() => {
          reject(createHttpErrorMock({ code, status }));
        }, 500);
      });
    })
    .persist();

export const getUpdateTagErrorMock = ({
  code,
  status,
}: {
  code?: string;
  status?: number;
} = {}) =>
  whenRequest(updateTag)
    .reply(200, async () => {
      return new Promise((_resolve, reject) => {
        window.setTimeout(() => {
          reject(createHttpErrorMock({ code, status }));
        }, 500);
      });
    })
    .persist();

export const getDeleteTagErrorMock = (code: string = 'ERR_NETWORK') =>
  whenRequest(deleteTag)
    .reply(200, async () => {
      return new Promise((_resolve, reject) => {
        window.setTimeout(() => {
          reject(new ServerError(code, 'error'));
        }, 500);
      });
    })
    .persist();

export default function createTagImdb({
  createdDate = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
  updatedDate = createdDate,
  fqdn = 'wix.patterns.dummyservice.v1.dummy_entity',
  ...overrides
}: Partial<TagType> = {}): TagType {
  return {
    createdDate,
    updatedDate,
    fqdn,
    ...overrides,
  };
}

export function createBackend(tags: TagType[]) {
  return new InMemoryBackend<TagType>({
    total: tags.length,
    paginationMode: 'offset',
    delay: { min: 100, max: 2000 },
    createOne: (index: number) => createTagImdb(tags[index]),
    predicate: (query) => {
      const filters = query.filters ?? {};
      const { fqdn } = filters;
      return (item) => item.fqdn === fqdn;
    },
    orderBy: (query) => {
      return [...(query.sort ?? [])];
    },
    itemKey: (item) => item.id!,
  });
}

export const tagsAPIHttpClientMocks = (tags: TagType[]) => {
  const tagsBackend = createBackend(tags);
  return [
    whenRequest(listTags)
      .reply(200, async ({ fqdn }) => {
        return tagsBackend
          .fetchData({
            filters: {
              fqdn,
            },
            limit: tags.length,
          })
          .then(({ items }) => {
            if (fqdn === 'wix.patterns.dummyservice.v1.empty') {
              return { tags: [] };
            } else if (fqdn === 'wix.patterns.dummyservice.v1.over_limit') {
              return { tags: aTagsArray(105) };
            } else {
              return { tags: items };
            }
          });
      })
      .persist(),
    whenRequest(queryTags)
      .reply(200, async ({ fqdn, query }) => {
        if (fqdn === 'wix.patterns.dummyservice.v1.empty') {
          return {
            tags: [],
            pagingMetadata: {
              count: 0,
              cursors: { next: null },
              hasNext: false,
            },
          };
        }
        const allItems =
          fqdn === 'wix.patterns.dummyservice.v1.over_limit'
            ? aTagsArray(105)
            : (await tagsBackend.fetchData({ filters: { fqdn }, limit: 1000 }))
                .items;
        const search = query?.filter?.name?.$startsWith as string | undefined;
        const filtered = search
          ? allItems.filter((tag) =>
              tag.name?.toLowerCase().startsWith(search.toLowerCase()),
            )
          : allItems;
        const limit = query?.cursorPaging?.limit ?? filtered.length;
        const cursorToken = query?.cursorPaging?.cursor;
        const offset = cursorToken ? parseInt(atob(cursorToken), 10) : 0;
        const pageItems = filtered.slice(offset, offset + limit);
        const nextOffset = offset + limit;
        const hasNext = nextOffset < filtered.length;
        return {
          tags: pageItems,
          pagingMetadata: {
            count: pageItems.length,
            cursors: { next: hasNext ? btoa(String(nextOffset)) : null },
            hasNext,
            total: filtered.length,
          },
        };
      })
      .persist(),
    whenRequest(createTag)
      .reply(200, ({ tag }) => {
        tagsBackend.push([tag]);
        return {
          __applicationErrorsType: {},
          tag,
        };
      })
      .persist(),
    whenRequest(updateTag)
      .reply(200, ({ tag }) => {
        tagsBackend.update(tag);
        return { tag };
      })
      .persist(),
    whenRequest(deleteTag)
      .reply(200, ({ tagId }) => {
        tagsBackend.map.delete(tagId);
      })
      .persist(),
  ];
};
