import { Mock } from 'ts-mockery';
import type { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
import type { ShlinkDomainRedirects } from '../../../src/api/types';
import { parseApiError } from '../../../src/api/utils';
import type { ShlinkState } from '../../../src/container/types';
import type { Domain } from '../../../src/domains/data';
import type { EditDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
import { editDomainRedirects } from '../../../src/domains/reducers/domainRedirects';
import type {
  DomainsList } from '../../../src/domains/reducers/domainsList';
import {
  domainsListReducerCreator,
  replaceRedirectsOnDomain,
  replaceStatusOnDomain,
} from '../../../src/domains/reducers/domainsList';
import type { SelectedServer, ServerData } from '../../../src/servers/data';

describe('domainsListReducer', () => {
  const dispatch = jest.fn();
  const getState = jest.fn();
  const listDomains = jest.fn();
  const health = jest.fn();
  const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ listDomains, health });
  const filteredDomains = [
    Mock.of<Domain>({ domain: 'foo', status: 'validating' }),
    Mock.of<Domain>({ domain: 'Boo', status: 'validating' }),
  ];
  const domains = [...filteredDomains, Mock.of<Domain>({ domain: 'bar', status: 'validating' })];
  const error = { type: 'NOT_FOUND', status: 404 } as unknown as Error;
  const editDomainRedirectsThunk = editDomainRedirects(buildShlinkApiClient);
  const { reducer, listDomains: listDomainsAction, checkDomainHealth, filterDomains } = domainsListReducerCreator(
    buildShlinkApiClient,
    editDomainRedirectsThunk,
  );

  beforeEach(jest.clearAllMocks);

  describe('reducer', () => {
    it('returns loading on LIST_DOMAINS_START', () => {
      expect(reducer(undefined, listDomainsAction.pending(''))).toEqual(
        { domains: [], filteredDomains: [], loading: true, error: false },
      );
    });

    it('returns error on LIST_DOMAINS_ERROR', () => {
      expect(reducer(undefined, listDomainsAction.rejected(error, ''))).toEqual(
        { domains: [], filteredDomains: [], loading: false, error: true, errorData: parseApiError(error) },
      );
    });

    it('returns domains on LIST_DOMAINS', () => {
      expect(
        reducer(undefined, listDomainsAction.fulfilled({ domains }, '')),
      ).toEqual({ domains, filteredDomains: domains, loading: false, error: false });
    });

    it('filters domains on FILTER_DOMAINS', () => {
      expect(reducer(Mock.of<DomainsList>({ domains }), filterDomains('oO'))).toEqual({ domains, filteredDomains });
    });

    it.each([
      ['foo'],
      ['bar'],
      ['does_not_exist'],
    ])('replaces redirects on proper domain on EDIT_DOMAIN_REDIRECTS', (domain) => {
      const redirects: ShlinkDomainRedirects = {
        baseUrlRedirect: 'bar',
        regular404Redirect: 'foo',
        invalidShortUrlRedirect: null,
      };
      const editDomainRedirects: EditDomainRedirects = { domain, redirects };

      expect(reducer(
        Mock.of<DomainsList>({ domains, filteredDomains }),
        editDomainRedirectsThunk.fulfilled(editDomainRedirects, '', editDomainRedirects),
      )).toEqual({
        domains: domains.map(replaceRedirectsOnDomain(editDomainRedirects)),
        filteredDomains: filteredDomains.map(replaceRedirectsOnDomain(editDomainRedirects)),
      });
    });

    it.each([
      ['foo'],
      ['bar'],
      ['does_not_exist'],
    ])('replaces status on proper domain on VALIDATE_DOMAIN', (domain) => {
      expect(reducer(
        Mock.of<DomainsList>({ domains, filteredDomains }),
        checkDomainHealth.fulfilled({ domain, status: 'valid' }, '', ''),
      )).toEqual({
        domains: domains.map(replaceStatusOnDomain(domain, 'valid')),
        filteredDomains: filteredDomains.map(replaceStatusOnDomain(domain, 'valid')),
      });
    });
  });

  describe('listDomains', () => {
    it('dispatches domains once loaded', async () => {
      listDomains.mockResolvedValue({ data: domains });

      await listDomainsAction()(dispatch, getState, {});

      expect(dispatch).toHaveBeenCalledTimes(2);
      expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
        payload: { domains },
      }));
      expect(listDomains).toHaveBeenCalledTimes(1);
    });
  });

  describe('filterDomains', () => {
    it.each([
      ['foo'],
      ['bar'],
      ['something'],
    ])('creates action as expected', (searchTerm) => {
      expect(filterDomains(searchTerm).payload).toEqual(searchTerm);
    });
  });

  describe('checkDomainHealth', () => {
    const domain = 'example.com';

    it('dispatches invalid status when selected server does not have all required data', async () => {
      getState.mockReturnValue(Mock.of<ShlinkState>({
        selectedServer: Mock.all<SelectedServer>(),
      }));

      await checkDomainHealth(domain)(dispatch, getState, {});

      expect(getState).toHaveBeenCalledTimes(1);
      expect(health).not.toHaveBeenCalled();
      expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
        payload: { domain, status: 'invalid' },
      }));
    });

    it('dispatches invalid status when health endpoint returns an error', async () => {
      getState.mockReturnValue(Mock.of<ShlinkState>({
        selectedServer: Mock.of<ServerData>({
          url: 'https://myerver.com',
          apiKey: '123',
        }),
      }));
      health.mockRejectedValue({});

      await checkDomainHealth(domain)(dispatch, getState, {});

      expect(getState).toHaveBeenCalledTimes(1);
      expect(health).toHaveBeenCalledTimes(1);
      expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
        payload: { domain, status: 'invalid' },
      }));
    });

    it.each([
      ['pass', 'valid'],
      ['fail', 'invalid'],
    ])('dispatches proper status based on status returned from health endpoint', async (
      healthStatus,
      expectedStatus,
    ) => {
      getState.mockReturnValue(Mock.of<ShlinkState>({
        selectedServer: Mock.of<ServerData>({
          url: 'https://myerver.com',
          apiKey: '123',
        }),
      }));
      health.mockResolvedValue({ status: healthStatus });

      await checkDomainHealth(domain)(dispatch, getState, {});

      expect(getState).toHaveBeenCalledTimes(1);
      expect(health).toHaveBeenCalledTimes(1);
      expect(dispatch).toHaveBeenLastCalledWith(expect.objectContaining({
        payload: { domain, status: expectedStatus },
      }));
    });
  });
});
