import { fireEvent, render, waitFor } from '@testing-library/react';
import { useFormikContext } from 'formik';
import React, { useEffect } from 'react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter, Route } from 'react-router-dom';
import type { Mock } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as Yup from 'yup';
import { noop } from '../../helpers/utils';
import { initializeUi } from '../../initialize';
import { ActionData } from '../Actions';
import { FormStation } from './FormStation';
import { ObjectSchemaDefinition } from './FormStation.models';

vi.mock('../../initialize');

/* eslint-disable @typescript-eslint/no-non-null-assertion */

const mockActions = (onActionSelected: Mock): ActionData[] => [
  {
    label: 'Action One',
    onActionSelected: onActionSelected,
  },
];

const defaultProps = {
  initialData: { loading: true },
  saveData: noop,
};

// Temporarily disable console.log() from FormStation until proper error handling
// vi.spyOn(console, 'log').mockImplementation(() => null);
vi.spyOn(console, 'error').mockImplementation(() => null);

const MakeDirty: React.FC = () => {
  const context = useFormikContext();
  useEffect(() => {
    // make the form dirty
    context.setFieldValue('dirty', 'yes');
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  return null;
};

describe('Details', () => {
  global.ResizeObserver = vi.fn().mockImplementation(function () {
    return { observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() };
  }) as unknown as typeof ResizeObserver;

  beforeEach(() => {
    vi.clearAllMocks();

    initializeUi({
      showNotification: () => {
        // not implemented
        return -1;
      },
      addIndicator: () => {
        return -1;
      },
      removeIndicator: noop,
      on: () => noop,
      setTitle: noop,
      setSaveIndicator: noop,
    });
  });

  it('renders the component without crashing', () => {
    const { container } = render(<FormStation {...defaultProps} />);

    expect(container).toBeTruthy();
  });

  describe('Actions', () => {
    it('hides actions by default', () => {
      const { container } = render(
        <MemoryRouter>
          <FormStation {...defaultProps} />
        </MemoryRouter>,
      );

      const actionsPanel = container.querySelector('[data-test-id="actions"]');

      expect(actionsPanel).not.toBeInTheDocument();
    });

    it('shows actions panel when alwaysShowActionsPanel is true', () => {
      const { container } = render(
        <MemoryRouter>
          <FormStation {...defaultProps} alwaysShowActionsPanel={true} />,
        </MemoryRouter>,
      );

      const actionsPanel = container.querySelector('[data-test-id="actions"]');

      expect(actionsPanel).toBeInTheDocument();
    });

    it('does not show actions panel when alwaysShowActionsPanel is false', () => {
      const { container } = render(
        <MemoryRouter>
          <FormStation {...defaultProps} alwaysShowActionsPanel={false} />,
        </MemoryRouter>,
      );

      const actionsPanel = container.querySelector('[data-test-id="actions"]');

      expect(actionsPanel).not.toBeInTheDocument();
    });

    it('does not show actions panel when alwaysShowActionsPanel is undefined', () => {
      const { container } = render(
        <MemoryRouter>
          <FormStation {...defaultProps} alwaysShowActionsPanel={undefined} />,
        </MemoryRouter>,
      );

      const actionsPanel = container.querySelector('[data-test-id="actions"]');

      expect(actionsPanel).not.toBeInTheDocument();
    });

    it('always shows Actions when at least one action is passed into prop "actions"', () => {
      const sampleActions = mockActions(vi.fn());
      const { container } = render(
        <MemoryRouter>
          <FormStation {...defaultProps} actions={sampleActions} />
        </MemoryRouter>,
      );
      const actions = container.querySelector('.actions');

      expect(actions).toBeInTheDocument();
    });

    describe('raising logic', () => {
      it('raises the action selected event if form is valid', async () => {
        const spy = vi.fn();
        const sampleActions = mockActions(spy);
        const { container } = render(
          <MemoryRouter>
            <FormStation {...defaultProps} actions={sampleActions} />
          </MemoryRouter>,
        );

        await waitFor(() => {
          const actionsPanel = container.querySelector(
            '[data-test-id="actions"]',
          );
          expect(actionsPanel).toBeInTheDocument();
        });

        const actionsPanel = container.querySelector(
          '[data-test-id="actions"]',
        );
        const actionButton = actionsPanel?.querySelector(
          '[data-test-id="action"]',
        ) as HTMLElement;

        await act(async () => {
          fireEvent.click(actionButton);
        });

        expect(spy).toHaveBeenCalledTimes(1);
      });

      it("doesn't raise the action selected event if the form is invalid", async () => {
        const spy = vi.fn();
        const sampleActions = mockActions(spy);
        const ForceValidation: React.FC = () => {
          const context = useFormikContext();
          useEffect(() => {
            context.validateForm();
            // eslint-disable-next-line react-hooks/exhaustive-deps
          }, []);
          return null;
        };

        const { container } = render(
          <MemoryRouter>
            <FormStation
              {...defaultProps}
              actions={sampleActions}
              validationSchema={Yup.object<ObjectSchemaDefinition<any>>({
                something: Yup.string().min(10).required(),
              })}
              initialData={{ loading: false, data: { something: '' } }}
            >
              <MakeDirty />
              <ForceValidation />
            </FormStation>
          </MemoryRouter>,
        );

        await act(async () => {
          // Wait for validation
          await new Promise((resolve) => setTimeout(resolve, 0));
        });

        await waitFor(() => {
          const actionsPanel = container.querySelector(
            '[data-test-id="actions"]',
          );
          expect(actionsPanel).toBeInTheDocument();
        });

        const actionsPanel = container.querySelector(
          '[data-test-id="actions"]',
        );
        const actionButton = actionsPanel?.querySelector(
          '[data-test-id="action"]',
        ) as HTMLElement;

        await act(async () => {
          fireEvent.click(actionButton);
        });

        expect(spy).not.toHaveBeenCalled();
      });

      it('does not raise the action selected event if saveData throws', async () => {
        const spy = vi.fn();
        const sampleActions = mockActions(spy);
        const { container } = render(
          <MemoryRouter>
            <FormStation
              {...defaultProps}
              actions={sampleActions}
              saveData={() => {
                throw new Error('fail');
              }}
              initialData={{ loading: false, data: {} }}
            >
              <MakeDirty />
            </FormStation>
          </MemoryRouter>,
        );

        await waitFor(() => {
          const actionsPanel = container.querySelector(
            '[data-test-id="actions"]',
          );
          expect(actionsPanel).toBeInTheDocument();
        });

        const actionsPanel = container.querySelector(
          '[data-test-id="actions"]',
        );
        const actionButton = actionsPanel?.querySelector(
          '[data-test-id="action"]',
        ) as HTMLElement;

        await act(async () => {
          fireEvent.click(actionButton);
        });

        expect(spy).not.toHaveBeenCalled();
      });

      it('does not attempt to save and call the action directly when form untouched', async () => {
        const spy = vi.fn();
        const sampleActions = mockActions(spy);
        const saveSpy = vi.fn();
        const { container } = render(
          <MemoryRouter>
            <FormStation
              {...defaultProps}
              actions={sampleActions}
              saveData={saveSpy}
              initialData={{ loading: false, data: {} }}
            ></FormStation>
          </MemoryRouter>,
        );

        let actionButton: HTMLElement | null = null;
        await waitFor(() => {
          const actionsPanel = container.querySelector(
            '[data-test-id="actions"]',
          );
          actionButton = actionsPanel?.querySelector(
            '[data-test-id="action"]',
          ) as HTMLElement;
          expect(actionButton).toBeInTheDocument();
        });

        await act(async () => {
          fireEvent.click(actionButton!);
        });

        expect(saveSpy).not.toHaveBeenCalled();
        expect(spy).toHaveBeenCalled();
      });
    });
  });

  describe('Header', () => {
    it('uses titleProperty as preferred header', () => {
      const { container } = render(
        <MemoryRouter>
          <FormStation
            {...defaultProps}
            saveData={() => {
              throw new Error('fail');
            }}
            initialData={{ loading: false, data: { title: 'Test title' } }}
            titleProperty="title"
            defaultTitle="default"
          ></FormStation>
        </MemoryRouter>,
      );

      const header = container.querySelector(
        '[data-test-id="page-header-title"]',
      );
      expect(header?.textContent).toContain('Test title');
    });

    it('uses defaultTitle as fallback', () => {
      const { container } = render(
        <MemoryRouter>
          <FormStation
            {...defaultProps}
            saveData={() => {
              throw new Error('fail');
            }}
            initialData={{
              loading: false,
              data: { title: 'Test title', prop: '' },
            }}
            titleProperty="prop"
            defaultTitle="default"
          ></FormStation>
        </MemoryRouter>,
      );

      const header = container.querySelector(
        '[data-test-id="page-header-title"]',
      );
      expect(header?.textContent).toContain('default');
    });

    describe('Reset Save and Cancel operations', () => {
      const ChangeValue: React.FC = () => {
        const context = useFormikContext();
        useEffect(() => {
          context.setFieldValue('something', 'changed');
          // eslint-disable-next-line react-hooks/exhaustive-deps
        }, []);
        return null;
      };

      it('allows resetting of a dirty form', async () => {
        let value = 'initial';

        const MonitorValue: React.FC = () => {
          const { values } = useFormikContext<{ something: string }>();
          useEffect(() => {
            value = values.something;
          }, [values]);
          return null;
        };

        const { container } = render(
          <MemoryRouter>
            <FormStation
              {...defaultProps}
              initialData={{ loading: false, data: { something: 'initial' } }}
            >
              <ChangeValue />
              <MonitorValue />
            </FormStation>
          </MemoryRouter>,
        );

        await act(async () => {
          await new Promise((resolve) => setTimeout(resolve, 0));
        });

        const headerActions = container.querySelectorAll(
          '[data-test-id="page-header-actions"] [data-test-id="action"]',
        );
        expect(headerActions).toHaveLength(2);

        fireEvent.click(headerActions[0]);

        expect(value).toBe('initial');
      });

      it('allows saving of a dirty form', async () => {
        let path: string;

        const { container } = render(
          <MemoryRouter>
            <FormStation
              {...defaultProps}
              initialData={{ loading: false, data: { something: 'initial' } }}
            >
              <ChangeValue />
            </FormStation>
            <Route
              path="*"
              render={({ location }) => {
                path = location.pathname;
                return null;
              }}
            />
          </MemoryRouter>,
        );
        await act(async () => {
          await new Promise((resolve) => setTimeout(resolve, 0));
        });

        const headerActions = container.querySelectorAll(
          '[data-test-id="page-header-actions"] [data-test-id="action"]',
        );
        expect(headerActions).toHaveLength(2);

        fireEvent.click(headerActions[1]);

        await act(async () => {
          await new Promise((resolve) => setTimeout(resolve, 0));
        });

        expect(path!).toBe('/');
      });

      it('allows cancellation if wanted', async () => {
        let path: string;

        const { container } = render(
          <MemoryRouter>
            <FormStation
              {...defaultProps}
              initialData={{ loading: false, data: { something: 'initial' } }}
              cancelNavigationUrl="/home"
              saveData={() => {
                // making sure that a call to save would fail
                throw new Error('fail');
              }}
            >
              <ChangeValue />
            </FormStation>
            <Route
              path="*"
              render={({ location }) => {
                path = location.pathname;
                return null;
              }}
            />
          </MemoryRouter>,
        );

        await act(async () => {
          await new Promise((resolve) => setTimeout(resolve, 0));
        });

        const headerActions = container.querySelectorAll(
          '[data-test-id="page-header-actions"] [data-test-id="action"]',
        );
        expect(headerActions).toHaveLength(3);

        fireEvent.click(headerActions[2]);

        await act(async () => {
          await new Promise((resolve) => setTimeout(resolve, 0));
        });

        expect(path!).toBe('/home');
      });
    });
  });

  describe('station message', () => {
    it('station message is not rendered by default', () => {
      const { container } = render(<FormStation {...defaultProps} />);

      const messageBar = container.querySelector(
        '[data-test-id="message-bar"]',
      );

      expect(messageBar).not.toBeInTheDocument();
    });

    it('renders a MessageBar component if stationMessage is set', () => {
      const { container } = render(
        <MemoryRouter>
          <FormStation
            {...defaultProps}
            initialData={{
              loading: false,
              data: { title: 'Test title', prop: '' },
            }}
            stationMessage={{ title: 'test message', type: 'info' }}
          ></FormStation>
        </MemoryRouter>,
      );

      const messageBar = container.querySelector(
        '[data-test-id="message-bar"]',
      );

      expect(messageBar).toBeInTheDocument();
    });

    it(`raises MessageBar's 'onClose' callback`, () => {
      const spy = vi.fn();
      const { container } = render(
        <MemoryRouter>
          <FormStation
            {...defaultProps}
            initialData={{
              loading: false,
              data: { title: 'Test title', prop: '' },
            }}
            stationMessage={{
              title: 'test message',
              type: 'info',
              onClose: spy,
            }}
          ></FormStation>
        </MemoryRouter>,
      );

      const closeButton = container.querySelector(
        '[data-test-id="message-bar-close-button"]',
      ) as HTMLButtonElement;

      fireEvent.click(closeButton);

      expect(spy).toHaveBeenCalledTimes(1);
    });
  });

  describe('station error', () => {
    it(`displays an error message when submitting throws an error and closes the message when 'X' is clicked`, async () => {
      const formError = new Error('Error on saving, not executing action');
      const spy = vi.fn();
      const sampleActions = mockActions(spy);

      const { container } = render(
        <MemoryRouter>
          <FormStation
            {...defaultProps}
            actions={sampleActions}
            saveData={() => {
              throw formError;
            }}
            initialData={{ loading: false, data: {} }}
          >
            <MakeDirty />
          </FormStation>
        </MemoryRouter>,
      );

      await waitFor(() => {
        const actionsPanel = container.querySelector(
          '[data-test-id="actions"]',
        );
        expect(actionsPanel).toBeInTheDocument();
      });

      const actionsPanel = container.querySelector('[data-test-id="actions"]');
      const actionButton = actionsPanel?.querySelector(
        '[data-test-id="action"]',
      ) as HTMLElement;

      await act(async () => {
        fireEvent.click(actionButton);
      });

      let messageBar: Element | null = null;
      await waitFor(() => {
        messageBar = container.querySelector('[data-test-id="message-bar"]');
        expect(messageBar).toBeInTheDocument();
      });

      const toggle = container.querySelector(
        '[data-test-id="message-bar-details-button"]',
      ) as HTMLElement;

      fireEvent.click(toggle);

      const body = container.querySelector('.body') as HTMLElement;

      expect(body.textContent).toContain(formError.message);

      const closeButton = container.querySelector(
        '[data-test-id="message-bar-close-button"]',
      ) as HTMLButtonElement;
      fireEvent.click(closeButton);

      messageBar = container.querySelector('.messageBar');

      expect(messageBar).not.toBeInTheDocument();
    });

    it(`displays an error body when a body is attached to the error object`, async () => {
      const formError = new Error('Submitting failed');
      const spy = vi.fn();
      const sampleActions = mockActions(spy);

      const { container } = render(
        <MemoryRouter>
          <FormStation
            {...defaultProps}
            actions={sampleActions}
            saveData={() => {
              throw formError;
            }}
            initialData={{ loading: false, data: {} }}
          >
            <MakeDirty />
          </FormStation>
        </MemoryRouter>,
      );

      await waitFor(() => {
        const actionsPanel = container.querySelector(
          '[data-test-id="actions"]',
        );
        expect(actionsPanel).toBeInTheDocument();
      });

      const actionsPanel = container.querySelector('[data-test-id="actions"]');
      const actionButton = actionsPanel?.querySelector(
        '[data-test-id="action"]',
      ) as HTMLElement;

      await act(async () => {
        fireEvent.click(actionButton);
      });

      let toggle: HTMLElement | null = null;
      await waitFor(() => {
        toggle = container.querySelector(
          '[data-test-id="message-bar-details-button"]',
        ) as HTMLElement;
        expect(toggle).toBeInTheDocument();
      });

      fireEvent.click(toggle!);

      const body = container.querySelector('.body') as HTMLElement;

      expect(body.textContent).toContain(formError.message);
    });
  });

  describe('Prevent multiple form submissions', () => {
    it(`disables actions and 'SaveOnNavigate' while a form is being submitted`, async () => {
      vi.useFakeTimers();
      const spy = vi.fn();
      const sampleActions = mockActions(spy);
      const onSubmit = (): Promise<{ id: number }> => {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve({ id: 3 });
          }, 1000);
        });
      };

      const { container } = render(
        <MemoryRouter>
          <FormStation
            {...defaultProps}
            actions={sampleActions}
            saveData={onSubmit}
            initialData={{ loading: false, data: {} }}
          >
            <MakeDirty />
          </FormStation>
        </MemoryRouter>,
      );

      // assert form actions are initially enabled
      await waitFor(() => {
        const actionsPanel = container.querySelector(
          '[data-test-id="actions"]',
        );
        expect(actionsPanel).toBeInTheDocument();
      });

      let actionsPanel = container.querySelector('[data-test-id="actions"]');
      let actionButton = actionsPanel?.querySelector(
        '[data-test-id="action"]',
      ) as HTMLElement;
      expect(actionButton).toBeInTheDocument();

      // submit form
      await act(async () => {
        fireEvent.click(actionButton);
      });

      // place form into 'submitting' state
      await act(async () => {
        vi.advanceTimersByTime(500);
      });

      // assert form actions are disabled while form is being submitted
      actionsPanel = container.querySelector('[data-test-id="actions"]');
      actionButton = actionsPanel?.querySelector(
        '[data-test-id="action"]',
      ) as HTMLElement;
      expect(actionButton).toHaveClass('isDisabled');

      // complete form submission
      await act(async () => {
        vi.runAllTimers();
      });

      // assert form actions are enabled again after form is submitted
      actionsPanel = container.querySelector('[data-test-id="actions"]');
      actionButton = actionsPanel?.querySelector(
        '[data-test-id="action"]',
      ) as HTMLElement;
      expect(actionButton).not.toHaveClass('isDisabled');
    });

    it(`enables actions and 'SaveOnNavigate' if form submission fails`, async () => {
      vi.useFakeTimers();
      const spy = vi.fn();
      const sampleActions = mockActions(spy);
      const onSubmit = (): Promise<{ id: number }> => {
        return new Promise((_resolve, reject) => {
          setTimeout(() => {
            reject();
          }, 1000);
        });
      };

      const { container } = render(
        <MemoryRouter>
          <FormStation
            {...defaultProps}
            actions={sampleActions}
            saveData={onSubmit}
            initialData={{ loading: false, data: {} }}
          >
            <MakeDirty />
          </FormStation>
        </MemoryRouter>,
      );

      // assert form actions are initially enabled
      let actionButton: HTMLElement | null = null;
      await waitFor(() => {
        const actionsPanel = container.querySelector(
          '[data-test-id="actions"]',
        );
        actionButton = actionsPanel?.querySelector(
          '[data-test-id="action"]',
        ) as HTMLElement;
        expect(actionButton).toBeInTheDocument();
      });

      expect(actionButton).toBeInTheDocument();

      // submit form
      await act(async () => {
        fireEvent.click(actionButton!);
      });

      // place form into 'submitting' state
      await act(async () => {
        vi.advanceTimersByTime(500);
      });

      // assert form actions are disabled while form is being submitted
      const actionsPanel2 = container.querySelector('[data-test-id="actions"]');
      actionButton = actionsPanel2?.querySelector(
        '[data-test-id="action"]',
      ) as HTMLElement;
      expect(actionButton).toHaveClass('isDisabled');

      // complete form submission
      await act(async () => {
        vi.runAllTimers();
      });

      // assert form actions are enabled again after form is submitted
      const actionsPanel3 = container.querySelector('[data-test-id="actions"]');
      actionButton = actionsPanel3?.querySelector(
        '[data-test-id="action"]',
      ) as HTMLElement;
      expect(actionButton).not.toHaveClass('isDisabled');
    });
  });
});
