import React from 'react';
import { Switch, Text } from 'react-native';

import { act, fireEvent, render, screen } from '@testing-library/react-native';
import type { Channel } from 'stream-chat';

import { ChannelDetailsContextProvider } from '../../../contexts/channelDetailsContext/channelDetailsContext';
import { ChatContext } from '../../../contexts/chatContext/ChatContext';
import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext';
import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
import { TranslationProvider } from '../../../contexts/translationContext/TranslationContext';
import type { ChannelActionItem } from '../../../hooks/actions/useChannelActionItems';
import * as useChannelActionsModule from '../../../hooks/actions/useChannelActions';
import * as useIsDirectChatModule from '../../../hooks/useIsDirectChat';
import * as useMutedUsersModule from '../../ChannelList/hooks/useMutedUsers';
import * as useIsChannelMutedModule from '../../ChannelPreview/hooks/useIsChannelMuted';
import type { ChannelDetailsActionItemProps } from '../components/ChannelDetailsActionItem';
import { ChannelDetailsActionsSection } from '../components/ChannelDetailsActionsSection';
import * as useChannelDetailsActionItemsModule from '../hooks/useChannelDetailsActionItems';

const NoopIcon = () => null;

const buildItem = (overrides: Partial<ChannelActionItem> = {}): ChannelActionItem => ({
  action: jest.fn(),
  Icon: NoopIcon,
  id: 'mute',
  label: 'Mute',
  placement: 'sheet',
  type: 'standard',
  ...overrides,
});

const channel = {
  cid: 'messaging:test',
  on: () => ({ unsubscribe: () => undefined }),
} as unknown as Channel;

type Probe = ChannelDetailsActionItemProps & { testID?: string };

const probeCalls: Probe[] = [];
const ActionItemProbe = (props: Probe) => {
  probeCalls.push(props);
  return (
    <>
      <Text testID={props.testID} onPress={props.onPress}>
        {props.label}
      </Text>
      {props.trailing}
    </>
  );
};

const sectionElement = () => (
  <ThemeProvider theme={defaultTheme}>
    <TranslationProvider
      value={{
        t: ((key: string) => key) as never,
        tDateTimeParser: ((input: unknown) => input) as never,
        userLanguage: 'en',
      }}
    >
      <ChatContext.Provider
        value={
          {
            client: {
              mutedUsers: [],
              on: () => ({ unsubscribe: () => undefined }),
              userID: 'me',
            },
          } as never
        }
      >
        <ChannelDetailsContextProvider channel={channel}>
          <WithComponents overrides={{ ChannelDetailsActionItem: ActionItemProbe }}>
            <ChannelDetailsActionsSection />
          </WithComponents>
        </ChannelDetailsContextProvider>
      </ChatContext.Provider>
    </TranslationProvider>
  </ThemeProvider>
);

const renderSection = () => render(sectionElement());

describe('ChannelDetailsActionsSection', () => {
  let useIsDirectChatSpy: jest.SpyInstance;
  let useActionItemsSpy: jest.SpyInstance;
  let useIsChannelMutedSpy: jest.SpyInstance;
  let useMutedUsersSpy: jest.SpyInstance;
  let getOtherUserSpy: jest.SpyInstance;

  beforeEach(() => {
    probeCalls.length = 0;
    useIsDirectChatSpy = jest
      .spyOn(useIsDirectChatModule, 'useIsDirectChat')
      .mockReturnValue(false);
    useActionItemsSpy = jest
      .spyOn(useChannelDetailsActionItemsModule, 'useChannelDetailsActionItems')
      .mockReturnValue([]);
    useIsChannelMutedSpy = jest
      .spyOn(useIsChannelMutedModule, 'useIsChannelMuted')
      .mockReturnValue({ createdAt: null, expiresAt: null, muted: false });
    useMutedUsersSpy = jest.spyOn(useMutedUsersModule, 'useMutedUsers').mockReturnValue([]);
    getOtherUserSpy = jest
      .spyOn(useChannelActionsModule, 'getOtherUserInDirectChannel')
      .mockReturnValue(undefined);
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  describe('when there are no items', () => {
    it('renders nothing', () => {
      const { toJSON } = renderSection();
      expect(toJSON()).toBeNull();
    });
  });

  describe('when there are items', () => {
    const muteItem = buildItem({ id: 'mute', label: 'Mute Group' });
    const leaveItem = buildItem({
      id: 'leave',
      label: 'Leave Group',
      type: 'destructive',
    });
    const deleteItem = buildItem({
      id: 'deleteChannel',
      label: 'Delete Group',
      type: 'destructive',
    });

    it('renders one list item per action item', () => {
      useActionItemsSpy.mockReturnValue([muteItem, leaveItem, deleteItem]);
      renderSection();
      expect(probeCalls).toHaveLength(3);
      expect(probeCalls.map((p) => p.label)).toEqual(['Mute Group', 'Leave Group', 'Delete Group']);
    });

    it('builds testIDs from the item id', () => {
      useActionItemsSpy.mockReturnValue([muteItem, leaveItem, deleteItem]);
      renderSection();
      expect(screen.getByTestId('channel-details-action-mute')).toBeTruthy();
      expect(screen.getByTestId('channel-details-action-leave')).toBeTruthy();
      expect(screen.getByTestId('channel-details-action-deleteChannel')).toBeTruthy();
    });

    it('forwards the icon, label, and onPress to ChannelDetailsActionItem', () => {
      useActionItemsSpy.mockReturnValue([leaveItem]);
      renderSection();
      const [item] = probeCalls;
      expect(item.Icon).toBe(leaveItem.Icon);
      expect(item.label).toBe('Leave Group');
      expect(typeof item.onPress).toBe('function');
    });

    it('passes destructive=true only for items with type="destructive"', () => {
      useActionItemsSpy.mockReturnValue([muteItem, leaveItem, deleteItem]);
      renderSection();
      const byId = Object.fromEntries(probeCalls.map((p) => [p.testID, p.destructive]));
      expect(byId['channel-details-action-mute']).toBe(false);
      expect(byId['channel-details-action-leave']).toBe(true);
      expect(byId['channel-details-action-deleteChannel']).toBe(true);
    });

    it('invokes the original action when a non-toggle list item is pressed', () => {
      const action = jest.fn();
      useActionItemsSpy.mockReturnValue([buildItem({ action, id: 'leave', label: 'Leave Group' })]);
      renderSection();
      fireEvent.press(screen.getByTestId('channel-details-action-leave'));
      expect(action).toHaveBeenCalledTimes(1);
    });

    it('does not forward onPress for the mute toggle row (Switch-driven)', () => {
      useActionItemsSpy.mockReturnValue([buildItem({ id: 'mute', label: 'Mute Group' })]);
      renderSection();
      const [item] = probeCalls;
      expect(item.onPress).toBeUndefined();
    });
  });

  describe('ChannelDetailsActionItem override', () => {
    it('uses the override passed via WithComponents instead of the default', () => {
      useActionItemsSpy.mockReturnValue([buildItem({ id: 'mute', label: 'Mute Group' })]);
      renderSection();
      // Probe is our injected override — its presence proves the override path is used.
      expect(probeCalls).toHaveLength(1);
    });
  });

  describe('mute / muteUser trailing Switch', () => {
    const leaveItem = buildItem({ id: 'leave', label: 'Leave Group', type: 'destructive' });

    it('passes a Switch as trailing only for mute and muteUser items', () => {
      useActionItemsSpy.mockReturnValue([
        buildItem({ id: 'mute', label: 'Mute Group' }),
        leaveItem,
      ]);
      renderSection();
      const byId = Object.fromEntries(probeCalls.map((p) => [p.testID, p.trailing]));
      expect(byId['channel-details-action-mute']).toBeTruthy();
      expect(byId['channel-details-action-leave']).toBeUndefined();
    });

    it('reflects channelMuted state on the mute item Switch', () => {
      useIsChannelMutedSpy.mockReturnValue({ createdAt: null, expiresAt: null, muted: true });
      useActionItemsSpy.mockReturnValue([buildItem({ id: 'mute', label: 'Unmute Group' })]);
      renderSection();
      const muteSwitch = screen.getByTestId('channel-details-action-mute-switch');
      expect(muteSwitch.props.value).toBe(true);
    });

    it('invokes the item action with an onFailure callback when the mute Switch is toggled', () => {
      const action = jest.fn();
      useActionItemsSpy.mockReturnValue([buildItem({ action, id: 'mute', label: 'Mute Group' })]);
      renderSection();
      const muteSwitch = screen.getByTestId('channel-details-action-mute-switch');
      fireEvent(muteSwitch, 'valueChange', true);
      expect(action).toHaveBeenCalledTimes(1);
      expect(typeof action.mock.calls[0][0].onFailure).toBe('function');
    });

    it('optimistically reflects the new value on the mute Switch before the action resolves', () => {
      useIsChannelMutedSpy.mockReturnValue({ createdAt: null, expiresAt: null, muted: false });
      useActionItemsSpy.mockReturnValue([
        buildItem({ action: jest.fn(), id: 'mute', label: 'Mute Group' }),
      ]);
      renderSection();
      const muteSwitch = screen.getByTestId('channel-details-action-mute-switch');
      expect(muteSwitch.props.value).toBe(false);
      fireEvent(muteSwitch, 'valueChange', true);
      expect(screen.getByTestId('channel-details-action-mute-switch').props.value).toBe(true);
    });

    it('rolls back the mute Switch when the action invokes onFailure', () => {
      const action = jest.fn();
      useIsChannelMutedSpy.mockReturnValue({ createdAt: null, expiresAt: null, muted: false });
      useActionItemsSpy.mockReturnValue([buildItem({ action, id: 'mute', label: 'Mute Group' })]);
      renderSection();
      const muteSwitch = screen.getByTestId('channel-details-action-mute-switch');
      fireEvent(muteSwitch, 'valueChange', true);
      expect(screen.getByTestId('channel-details-action-mute-switch').props.value).toBe(true);
      act(() => {
        action.mock.calls[0][0].onFailure();
      });
      expect(screen.getByTestId('channel-details-action-mute-switch').props.value).toBe(false);
    });

    it('rolls back the mute Switch to the current hook value (not !value) when it changed mid-flight', () => {
      const action = jest.fn();
      useIsChannelMutedSpy.mockReturnValue({ createdAt: null, expiresAt: null, muted: false });
      useActionItemsSpy.mockReturnValue([buildItem({ action, id: 'mute', label: 'Mute Group' })]);
      const { rerender } = renderSection();
      const muteSwitch = screen.getByTestId('channel-details-action-mute-switch');
      // Optimistically flip on while the mute request is in flight.
      fireEvent(muteSwitch, 'valueChange', true);
      expect(screen.getByTestId('channel-details-action-mute-switch').props.value).toBe(true);
      // A server event reports the channel as muted before the request resolves.
      useIsChannelMutedSpy.mockReturnValue({ createdAt: null, expiresAt: null, muted: true });
      rerender(sectionElement());
      // The request fails: revert to the current hook value (true), not !value (false).
      act(() => {
        action.mock.calls[0][0].onFailure();
      });
      expect(screen.getByTestId('channel-details-action-mute-switch').props.value).toBe(true);
    });

    it('reflects userMuted state on the muteUser item Switch in direct chats', () => {
      useIsDirectChatSpy.mockReturnValue(true);
      getOtherUserSpy.mockReturnValue({ user: { id: 'other-user' } });
      useMutedUsersSpy.mockReturnValue([{ target: { id: 'other-user' }, user: { id: 'me' } }]);
      useActionItemsSpy.mockReturnValue([buildItem({ id: 'muteUser', label: 'Unmute User' })]);
      renderSection();
      const userMuteSwitch = screen.getByTestId('channel-details-action-muteUser-switch');
      expect(userMuteSwitch.props.value).toBe(true);
    });

    it('optimistically updates and rolls back the muteUser Switch on failure', () => {
      const action = jest.fn();
      useIsDirectChatSpy.mockReturnValue(true);
      getOtherUserSpy.mockReturnValue({ user: { id: 'other-user' } });
      useMutedUsersSpy.mockReturnValue([]);
      useActionItemsSpy.mockReturnValue([
        buildItem({ action, id: 'muteUser', label: 'Mute User' }),
      ]);
      renderSection();
      const userMuteSwitch = screen.getByTestId('channel-details-action-muteUser-switch');
      expect(userMuteSwitch.props.value).toBe(false);
      fireEvent(userMuteSwitch, 'valueChange', true);
      expect(screen.getByTestId('channel-details-action-muteUser-switch').props.value).toBe(true);
      expect(action).toHaveBeenCalledTimes(1);
      act(() => {
        action.mock.calls[0][0].onFailure();
      });
      expect(screen.getByTestId('channel-details-action-muteUser-switch').props.value).toBe(false);
    });

    it('rolls back the muteUser Switch to the current hook value (not !value) when it changed mid-flight', () => {
      const action = jest.fn();
      useIsDirectChatSpy.mockReturnValue(true);
      getOtherUserSpy.mockReturnValue({ user: { id: 'other-user' } });
      useMutedUsersSpy.mockReturnValue([]);
      useActionItemsSpy.mockReturnValue([
        buildItem({ action, id: 'muteUser', label: 'Mute User' }),
      ]);
      const { rerender } = renderSection();
      const userMuteSwitch = screen.getByTestId('channel-details-action-muteUser-switch');
      // Optimistically flip on while the mute request is in flight.
      fireEvent(userMuteSwitch, 'valueChange', true);
      expect(screen.getByTestId('channel-details-action-muteUser-switch').props.value).toBe(true);
      // A server event reports the user as muted before the request resolves.
      useMutedUsersSpy.mockReturnValue([{ target: { id: 'other-user' }, user: { id: 'me' } }]);
      rerender(sectionElement());
      // The request fails: revert to the current hook value (true), not !value (false).
      act(() => {
        action.mock.calls[0][0].onFailure();
      });
      expect(screen.getByTestId('channel-details-action-muteUser-switch').props.value).toBe(true);
    });

    it('userMuted is false when the other user is not in mutedUsers', () => {
      useIsDirectChatSpy.mockReturnValue(true);
      getOtherUserSpy.mockReturnValue({ user: { id: 'other-user' } });
      useMutedUsersSpy.mockReturnValue([{ target: { id: 'someone-else' }, user: { id: 'me' } }]);
      useActionItemsSpy.mockReturnValue([buildItem({ id: 'muteUser', label: 'Mute User' })]);
      renderSection();
      const userMuteSwitch = screen.getByTestId('channel-details-action-muteUser-switch');
      expect(userMuteSwitch.props.value).toBe(false);
    });

    it('renders Switch components in the tree for mute toggles', () => {
      useActionItemsSpy.mockReturnValue([buildItem({ id: 'mute', label: 'Mute Group' })]);
      const { UNSAFE_getAllByType } = renderSection();
      expect(UNSAFE_getAllByType(Switch)).toHaveLength(1);
    });
  });
});
