import { useState, MouseEvent, ReactElement } from 'react';
import { userEvent, within, expect, waitFor } from 'storybook/test';
import { Meta, StoryObj } from '@storybook/react-webpack5';
import { action } from 'storybook/actions';
import { Star } from '@transferwise/icons';
import { InfoPrompt, type InfoPromptProps } from './InfoPrompt';
import { allModes } from '../../../.storybook/modes';
import { withVariantConfig } from '../../../.storybook/helpers';

const withComponentGrid =
  ({ gap = '1rem' } = {}) =>
  (Story: () => ReactElement) => (
    <div style={{ display: 'flex', flexDirection: 'column', gap }}>
      <Story />
    </div>
  );

const meta = {
  title: 'Prompts/InfoPrompt/Tests',
  component: InfoPrompt,
  tags: ['!autodocs', '!manifest', 'new'],
  args: {
    sentiment: 'neutral',
    description: 'This is an informational message.',
  },
} satisfies Meta<typeof InfoPrompt>;

export default meta;

type Story = StoryObj<typeof meta>;

const wait = async (duration = 500) =>
  new Promise<void>((resolve) => {
    setTimeout(resolve, duration);
  });

/**
 * Test that clicking the dismiss button removes the prompt.
 */
export const DismissInteraction: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify prompt is visible', async () => {
      await waitFor(async () =>
        expect(canvas.getByText('This message can be dismissed.')).toBeInTheDocument(),
      );
    });

    await step('Click the dismiss button', async () => {
      const dismissButton = canvas.getByRole('button', { name: /close/i, hidden: true });
      await userEvent.click(dismissButton);
    });

    await step('Verify prompt is removed', async () => {
      await waitFor(async () =>
        expect(canvas.queryByText('This message can be dismissed.')).not.toBeInTheDocument(),
      );
    });
  },
  render: function Render(args: InfoPromptProps) {
    const [isVisible, setIsVisible] = useState(true);

    if (!isVisible) {
      return <div data-testid="dismissed">Prompt dismissed</div>;
    }

    return (
      <InfoPrompt
        {...args}
        description="This message can be dismissed."
        onDismiss={() => setIsVisible(false)}
      />
    );
  },
};

/**
 * Test keyboard accessibility - dismiss prompt via Enter key.
 */
export const DismissViaKeyboard: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Tab to the dismiss button', async () => {
      await wait();
      await userEvent.tab();
    });

    await step('Press Enter to dismiss', async () => {
      await userEvent.keyboard('{Enter}');
    });

    await step('Verify prompt is removed', async () => {
      await waitFor(async () =>
        expect(canvas.queryByText('Press Enter to dismiss.')).not.toBeInTheDocument(),
      );
    });
  },
  render: function Render(args: InfoPromptProps) {
    const [isVisible, setIsVisible] = useState(true);

    if (!isVisible) {
      return <div data-testid="dismissed">Prompt dismissed</div>;
    }

    return (
      <InfoPrompt
        {...args}
        description="Press Enter to dismiss."
        onDismiss={() => setIsVisible(false)}
      />
    );
  },
};

/**
 * Test action link click interaction.
 */
export const ActionClickInteraction: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify prompt with action is visible', async () => {
      await waitFor(async () =>
        expect(canvas.getByText('Click the action link.')).toBeInTheDocument(),
      );
    });

    await step('Click the action link', async () => {
      const actionLink = await waitFor(() => canvas.getByRole('link', { name: 'Learn more' }));
      await userEvent.click(actionLink);
    });

    await step('Verify action was triggered', async () => {
      await waitFor(async () => expect(canvas.getByText('Action clicked!')).toBeInTheDocument());
    });
  },
  render: function Render(args: InfoPromptProps) {
    const [actionClicked, setActionClicked] = useState(false);

    return (
      <>
        <InfoPrompt
          {...args}
          description="Click the action link."
          action={{
            label: 'Learn more',
            href: '#',
            onClick: (e: MouseEvent) => {
              e.preventDefault();
              setActionClicked(true);
            },
          }}
        />
        {actionClicked && <div>Action clicked!</div>}
      </>
    );
  },
};

/**
 * Test multiple prompts with different sentiments can be dismissed independently.
 */
export const MultipleDismissInteraction: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify all prompts are visible', async () => {
      await waitFor(async () => {
        await expect(canvas.getByText('Success message')).toBeInTheDocument();
        await expect(canvas.getByText('Warning message')).toBeInTheDocument();
        await expect(canvas.getByText('Negative message')).toBeInTheDocument();
      });
    });

    await step('Dismiss the warning prompt', async () => {
      // Click the second dismiss button (warning)
      const dismissButtons = canvas.getAllByRole('button', { name: /close/i, hidden: true });
      await userEvent.click(dismissButtons[1]);
    });

    await step('Verify only warning prompt is removed', async () => {
      await waitFor(async () => {
        await expect(canvas.getByText('Success message')).toBeInTheDocument();
        await expect(canvas.queryByText('Warning message')).not.toBeInTheDocument();
        await expect(canvas.getByText('Negative message')).toBeInTheDocument();
      });
    });
  },
  render: function Render() {
    const [prompts, setPrompts] = useState([
      { id: 1, sentiment: 'success' as const, description: 'Success message' },
      { id: 2, sentiment: 'warning' as const, description: 'Warning message' },
      { id: 3, sentiment: 'negative' as const, description: 'Negative message' },
    ]);

    const handleDismiss = (id: number) => {
      setPrompts((prev) => prev.filter((p) => p.id !== id));
    };

    return (
      <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
        {prompts.map((prompt) => (
          <InfoPrompt
            key={prompt.id}
            sentiment={prompt.sentiment}
            description={prompt.description}
            onDismiss={() => handleDismiss(prompt.id)}
          />
        ))}
      </div>
    );
  },
};

/**
 * Test that touch interactions work for navigation (mobile).
 */
export const TouchInteraction: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify prompt with action is visible', async () => {
      await waitFor(async () => expect(canvas.getByText('Tap the prompt.')).toBeInTheDocument());
    });

    await step('Click the action (simulating touch)', async () => {
      const actionLink = await waitFor(() => canvas.getByRole('link', { name: 'Navigate' }));
      await userEvent.click(actionLink);
    });

    await step('Verify navigation was triggered', async () => {
      await waitFor(async () => expect(canvas.getByText('Navigated!')).toBeInTheDocument());
    });
  },
  render: function Render(args: InfoPromptProps) {
    const [navigated, setNavigated] = useState(false);

    return (
      <>
        <InfoPrompt
          {...args}
          description="Tap the prompt."
          action={{
            label: 'Navigate',
            href: '#',
            onClick: (e: MouseEvent) => {
              e.preventDefault();
              setNavigated(true);
            },
          }}
        />
        {navigated && <div>Navigated!</div>}
      </>
    );
  },
};

export const AllThemesAndSentiments: Story = {
  render: () => (
    <>
      {(['success', 'warning', 'negative', 'neutral', 'proposition'] as const).map((sentiment) => (
        <InfoPrompt
          key={sentiment}
          sentiment={sentiment}
          title={`${sentiment.charAt(0).toUpperCase() + sentiment.slice(1)} prompt`}
          description="This demonstrates the prompt appearance in different themes."
          media={sentiment === 'success' ? { asset: <Star title="Star" /> } : undefined}
          action={{ label: 'Action', onClick: action('action') }}
          onDismiss={action('dismiss')}
        />
      ))}
    </>
  ),
  decorators: [withComponentGrid({ gap: '1.5rem' })],
  parameters: {
    padding: '16px',
    variants: ['default', 'dark', 'bright-green', 'forest-green'],
    chromatic: {
      dark: allModes.dark,
      brightGreen: allModes.brightGreen,
      forestGreen: allModes.forestGreen,
    },
  },
};

export const TinyScreen: Story = {
  render: () => (
    <InfoPrompt
      sentiment="success"
      title="Payment received"
      description="Your transfer of 500 GBP has been successfully received and is now available in your account."
      action={{ label: 'View details', onClick: action('view-details') }}
      onDismiss={action('dismiss')}
    />
  ),
  ...withVariantConfig(['400%']),
};

/**
 * Test that the default LiveRegion renders with role="status" and aria-live="polite".
 */
export const LiveRegionPoliteDefault: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify live region with role="status" exists', async () => {
      await waitFor(async () => {
        const liveRegion = canvas.getByRole('status');
        await expect(liveRegion).toBeInTheDocument();
        await waitFor(async () => expect(liveRegion).toHaveAttribute('aria-live', 'polite'));
        await expect(liveRegion).toHaveAttribute('aria-atomic', 'true');
      });
    });

    await step('Verify prompt content is inside the live region', async () => {
      const liveRegion = canvas.getByRole('status');
      await expect(within(liveRegion).getByText('Polite announcement')).toBeInTheDocument();
    });
  },
  args: {
    description: 'Polite announcement',
  },
};

/**
 * Test that aria-live="assertive" renders with role="alert".
 */
export const LiveRegionAssertive: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify live region with role="alert" exists', async () => {
      await waitFor(async () => {
        const liveRegion = canvas.getByRole('alert');
        await expect(liveRegion).toBeInTheDocument();
        await waitFor(async () => expect(liveRegion).toHaveAttribute('aria-live', 'assertive'));
        await expect(liveRegion).toHaveAttribute('aria-atomic', 'true');
      });
    });

    await step('Verify prompt content is inside the live region', async () => {
      const liveRegion = canvas.getByRole('alert');
      await expect(within(liveRegion).getByText('Payment failed')).toBeInTheDocument();
    });
  },
  args: {
    sentiment: 'negative',
    description: 'Payment failed',
    'aria-live': 'assertive',
  },
};

/**
 * Test that a polite InfoPrompt added to the DOM gets a delayed live region.
 */
export const LiveRegionPoliteWhenAddedToDom: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify prompt is not in the DOM before trigger', async () => {
      await expect(canvas.queryByText('Polite prompt added later')).not.toBeInTheDocument();
      await expect(canvas.queryByRole('status')).not.toBeInTheDocument();
    });

    await step('Add two prompts to the DOM', async () => {
      const addButton = canvas.getByRole('button', { name: 'Add polite prompt' });
      await userEvent.click(addButton);
      await userEvent.click(addButton);
    });

    await step('Verify delayed polite live regions appear with content', async () => {
      await expect(canvas.getByText('Polite prompt added later 1')).toBeInTheDocument();
      await expect(canvas.getByText('Polite prompt added later 2')).toBeInTheDocument();
      const initialLiveRegions = canvas.queryAllByRole('status');
      await expect(initialLiveRegions).toHaveLength(2);
      await expect(initialLiveRegions[0].firstElementChild).toHaveAttribute('aria-hidden', 'true');
      await expect(initialLiveRegions[1].firstElementChild).toHaveAttribute('aria-hidden', 'true');

      await waitFor(async () => {
        const liveRegions = canvas.getAllByRole('status');
        await expect(liveRegions).toHaveLength(2);
        await expect(liveRegions[0]).toHaveAttribute('aria-live', 'polite');
        await expect(liveRegions[1]).toHaveAttribute('aria-live', 'polite');
        await expect(liveRegions[0]).toHaveAttribute('aria-atomic', 'true');
        await expect(liveRegions[1]).toHaveAttribute('aria-atomic', 'true');
        await expect(liveRegions[0].firstElementChild).not.toHaveAttribute('aria-hidden');
        await expect(liveRegions[1].firstElementChild).not.toHaveAttribute('aria-hidden');
        await expect(
          within(liveRegions[0]).getByText('Polite prompt added later 1'),
        ).toBeInTheDocument();
        await expect(
          within(liveRegions[1]).getByText('Polite prompt added later 2'),
        ).toBeInTheDocument();
      });
    });
  },
  render: function Render(args: InfoPromptProps) {
    const [promptIndexes, setPromptIndexes] = useState<number[]>([]);

    const addPrompt = () => {
      setPromptIndexes((previousIndexes) => [...previousIndexes, previousIndexes.length + 1]);
    };

    return (
      <>
        <button type="button" onClick={addPrompt}>
          Add polite prompt
        </button>
        {promptIndexes.map((promptIndex) => (
          <InfoPrompt
            key={`polite-${promptIndex}`}
            {...args}
            description={`Polite prompt added later ${promptIndex}`}
          />
        ))}
      </>
    );
  },
};

/**
 * Test that an assertive InfoPrompt added to the DOM gets a delayed live region.
 */
export const LiveRegionAssertiveWhenAddedToDom: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify prompt is not in the DOM before trigger', async () => {
      await expect(canvas.queryByText('Assertive prompt added later')).not.toBeInTheDocument();
      await expect(canvas.queryByRole('alert')).not.toBeInTheDocument();
    });

    await step('Add two prompts to the DOM', async () => {
      const addButton = canvas.getByRole('button', { name: 'Add assertive prompt' });
      await userEvent.click(addButton);
      await userEvent.click(addButton);
    });

    await step('Verify delayed assertive live regions appear with content', async () => {
      await expect(canvas.getByText('Assertive prompt added later 1')).toBeInTheDocument();
      await expect(canvas.getByText('Assertive prompt added later 2')).toBeInTheDocument();
      const initialLiveRegions = canvas.queryAllByRole('alert');
      await expect(initialLiveRegions).toHaveLength(2);
      await expect(initialLiveRegions[0].firstElementChild).toHaveAttribute('aria-hidden', 'true');
      await expect(initialLiveRegions[1].firstElementChild).toHaveAttribute('aria-hidden', 'true');

      await waitFor(async () => {
        const liveRegions = canvas.getAllByRole('alert');
        await expect(liveRegions).toHaveLength(2);
        await expect(liveRegions[0]).toHaveAttribute('aria-live', 'assertive');
        await expect(liveRegions[1]).toHaveAttribute('aria-live', 'assertive');
        await expect(liveRegions[0]).toHaveAttribute('aria-atomic', 'true');
        await expect(liveRegions[1]).toHaveAttribute('aria-atomic', 'true');
        await expect(liveRegions[0].firstElementChild).not.toHaveAttribute('aria-hidden');
        await expect(liveRegions[1].firstElementChild).not.toHaveAttribute('aria-hidden');
        await expect(
          within(liveRegions[0]).getByText('Assertive prompt added later 1'),
        ).toBeInTheDocument();
        await expect(
          within(liveRegions[1]).getByText('Assertive prompt added later 2'),
        ).toBeInTheDocument();
      });
    });
  },
  render: function Render(args: InfoPromptProps) {
    const [promptIndexes, setPromptIndexes] = useState<number[]>([]);

    const addPrompt = () => {
      setPromptIndexes((previousIndexes) => [...previousIndexes, previousIndexes.length + 1]);
    };

    return (
      <>
        <button type="button" onClick={addPrompt}>
          Add assertive prompt
        </button>
        {promptIndexes.map((promptIndex) => (
          <InfoPrompt
            key={`assertive-${promptIndex}`}
            {...args}
            description={`Assertive prompt added later ${promptIndex}`}
            aria-live="assertive"
          />
        ))}
      </>
    );
  },
};

/**
 * Test that aria-live="off" renders without a live region wrapper.
 */
export const LiveRegionOff: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify no live region wrapper exists', async () => {
      await waitFor(async () => {
        await expect(canvas.queryByRole('status')).not.toBeInTheDocument();
        await expect(canvas.queryByRole('alert')).not.toBeInTheDocument();
      });
    });

    await step('Verify prompt content still renders', async () => {
      await expect(canvas.getByText('Static info')).toBeInTheDocument();
    });
  },
  args: {
    description: 'Static info',
    'aria-live': 'off',
  },
};

/**
 * Test that a dismissed prompt also removes the live region.
 */
export const LiveRegionDismiss: Story = {
  play: async ({ canvasElement, step }) => {
    const canvas = within(canvasElement);

    await step('Verify live region exists before dismiss', async () => {
      await waitFor(async () => {
        await expect(canvas.getByRole('status')).toBeInTheDocument();
      });
    });

    await step('Dismiss the prompt', async () => {
      const dismissButton = canvas.getByRole('button', { name: /close/i, hidden: true });
      await userEvent.click(dismissButton);
    });

    await step('Verify live region is removed', async () => {
      await waitFor(async () => {
        await expect(canvas.queryByRole('status')).not.toBeInTheDocument();
      });
    });
  },
  render: function Render(args: InfoPromptProps) {
    const [isVisible, setIsVisible] = useState(true);

    if (!isVisible) {
      return <div data-testid="dismissed">Prompt dismissed</div>;
    }

    return (
      <InfoPrompt
        {...args}
        description="Dismissable live region"
        onDismiss={() => setIsVisible(false)}
      />
    );
  },
};
