import React from 'react';
import { Platform } from 'react-native';
import { fireEvent, render, within } from '@testing-library/react-native';
import { Provider } from 'react-redux';

import CallView from '.';
import { navigateToCallRoom } from '../../lib/services/voip/navigateToCallRoom';
import { useCallStore } from '../../lib/services/voip/useCallStore';
import { mockedStore } from '../../reducers/mockedStore';
import * as stories from './CallView.stories';
import { generateSnapshots } from '../../../.rnstorybook/generateSnapshots';

const mockStartRingback = jest.fn(() => Promise.resolve());
const mockStopRingback = jest.fn(() => Promise.resolve());

jest.mock('../../containers/Ringer', () => {
	const ReactMod = require('react');
	const RingerMock = (props: { ringer: string }) => ReactMod.createElement('view', { testID: `ringer-${props.ringer}` });
	return {
		__esModule: true,
		default: RingerMock,
		ERingerSounds: { DIALTONE: 'dialtone', RINGTONE: 'ringtone' }
	};
});

jest.mock('../../lib/native/NativeVoip', () => ({
	__esModule: true,
	default: {
		registerVoipToken: jest.fn(),
		getInitialEvents: jest.fn(() => null),
		clearInitialEvents: jest.fn(),
		getLastVoipToken: jest.fn(() => ''),
		stopNativeDDPClient: jest.fn(),
		stopVoipCallService: jest.fn(),
		setSpeakerOn: jest.fn(() => Promise.resolve(true)),
		startAudioRouteSync: jest.fn(() => Promise.resolve()),
		stopAudioRouteSync: jest.fn(() => Promise.resolve()),
		startRingback: () => mockStartRingback(),
		stopRingback: () => mockStopRingback(),
		addListener: jest.fn(),
		removeListeners: jest.fn()
	}
}));

// Re-evaluate `isIOS` per-test from Platform.OS (the helper computes it once at import time).
jest.mock('../../lib/methods/helpers', () => {
	const actual = jest.requireActual('../../lib/methods/helpers');
	const { Platform: RNPlatform } = jest.requireActual('react-native');
	const proxy: Record<string, unknown> = { ...actual };
	Object.defineProperty(proxy, 'isIOS', {
		get() {
			return RNPlatform.OS === 'ios';
		},
		enumerable: true,
		configurable: true
	});
	Object.defineProperty(proxy, 'isAndroid', {
		get() {
			return RNPlatform.OS !== 'ios';
		},
		enumerable: true,
		configurable: true
	});
	return proxy;
});

let mockWindowWidth = 350;
let mockWindowHeight = 800;
jest.mock('react-native/Libraries/Utilities/useWindowDimensions', () => ({
	__esModule: true,
	default: () => ({ width: mockWindowWidth, height: mockWindowHeight, scale: 1, fontScale: 1 })
}));

const mockNavigateToCallRoom = jest.mocked(navigateToCallRoom);

jest.mock('../../lib/services/voip/navigateToCallRoom', () => ({
	navigateToCallRoom: jest.fn().mockResolvedValue(undefined)
}));

// Mock useResponsiveLayout so its width tracks mockWindowWidth dynamically.
// Honors an explicit ResponsiveLayoutContext.Provider (e.g. TabletCallView story
// forcing width=800) so stories can drive layoutMode without a prop.
jest.mock('../../lib/hooks/useResponsiveLayout/useResponsiveLayout', () => {
	const actual = jest.requireActual('../../lib/hooks/useResponsiveLayout/useResponsiveLayout');
	const React = require('react');
	const { useWindowDimensions } = require('react-native');
	return {
		...actual,
		useResponsiveLayout: () => {
			const ctx = React.useContext(actual.ResponsiveLayoutContext);
			const { width: winWidth, height: winHeight, fontScale: winFontScale } = useWindowDimensions();
			const width = ctx && ctx.width ? ctx.width : winWidth;
			const height = ctx && ctx.height ? ctx.height : winHeight;
			const fontScale = ctx && ctx.fontScale ? ctx.fontScale : winFontScale;
			const isLargeFontScale = fontScale > actual.FONT_SCALE_LIMIT;
			const fontScaleLimited = isLargeFontScale ? actual.FONT_SCALE_LIMIT : fontScale;
			return {
				fontScale,
				width,
				height,
				isLargeFontScale,
				fontScaleLimited,
				rowHeight: actual.BASE_ROW_HEIGHT * fontScale,
				rowHeightCondensed: actual.BASE_ROW_HEIGHT_CONDENSED * fontScale
			};
		}
	};
});

const mockShowActionSheetRef = jest.fn();
jest.mock('../../containers/ActionSheet', () => ({
	showActionSheetRef: (options: any) => mockShowActionSheetRef(options),
	hideActionSheetRef: jest.fn(),
	ActionSheetProvider: ({ children }: { children: React.ReactNode }) => children
}));

// Helper to create a mock call
const createMockCall = (overrides: any = {}) => ({
	state: 'active',
	muted: false,
	held: false,
	contact: {
		displayName: 'Bob Burnquist',
		username: 'bob.burnquist',
		sipExtension: ''
	},
	setMuted: jest.fn(),
	setHeld: jest.fn(),
	hangup: jest.fn(),
	reject: jest.fn(),
	emitter: {
		on: jest.fn(),
		off: jest.fn()
	},
	...overrides
});

// Helper to set store state for tests
const setStoreState = (overrides: Partial<ReturnType<typeof useCallStore.getState>> = {}) => {
	const mockCall = createMockCall();
	useCallStore.setState({
		call: mockCall as any,
		callId: 'test-id',
		callState: 'active',
		isMuted: false,
		isOnHold: false,
		isSpeakerOn: false,
		callStartTime: Date.now(),
		contact: {
			displayName: 'Bob Burnquist',
			username: 'bob.burnquist',
			sipExtension: ''
		},
		roomId: 'test-room-rid',
		...overrides
	});
};

const Wrapper = ({ children }: { children: React.ReactNode }) => <Provider store={mockedStore}>{children}</Provider>;

describe('CallView/CallView', () => {
	beforeEach(() => {
		mockWindowWidth = 350;
		mockWindowHeight = 800;
		useCallStore.getState().reset();
		jest.clearAllMocks();
	});

	it('should return null when there is no call', () => {
		useCallStore.setState({ call: null });
		const { queryByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);
		expect(queryByTestId('caller-info-toggle')).toBeNull();
	});

	it('should render when there is a call', () => {
		setStoreState();
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		expect(getByTestId('caller-info-toggle')).toBeTruthy();
		expect(getByTestId('call-view-speaker')).toBeTruthy();
		expect(getByTestId('call-view-hold')).toBeTruthy();
		expect(getByTestId('call-view-mute')).toBeTruthy();
		expect(getByTestId('call-view-message')).toBeTruthy();
		expect(getByTestId('call-view-end')).toBeTruthy();
		expect(getByTestId('call-view-dialpad')).toBeTruthy();
	});

	it('should show CallStatusText when call is active', () => {
		setStoreState({ callState: 'active' });
		const { getByText } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		// CallStatusText should render (it shows a space when not muted/on hold)
		expect(getByText(/\s/)).toBeTruthy();
	});

	it('should not show CallStatusText when call is not active', () => {
		setStoreState({ callState: 'ringing' });
		const { queryByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		// CallStatusText should not be rendered when not active
		// We can't easily test for the Text component directly, so we verify the caller-info-toggle is rendered
		// but CallStatusText won't be in the tree when callState is not 'active'
		expect(queryByTestId('caller-info-toggle')).toBeTruthy();
	});

	it('should disable hold and mute buttons when call is connecting', () => {
		setStoreState({ callState: 'ringing' });
		const toggleHold = jest.fn();
		const toggleMute = jest.fn();
		useCallStore.setState({ toggleHold, toggleMute });

		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		const holdButton = getByTestId('call-view-hold');
		const muteButton = getByTestId('call-view-mute');

		// Test that disabled buttons don't trigger onPress
		fireEvent.press(holdButton);
		fireEvent.press(muteButton);
		expect(toggleHold).not.toHaveBeenCalled();
		expect(toggleMute).not.toHaveBeenCalled();
	});

	it('should enable hold and mute buttons when call is active', () => {
		setStoreState({ callState: 'active' });
		const toggleHold = jest.fn();
		const toggleMute = jest.fn();
		useCallStore.setState({ toggleHold, toggleMute });

		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		const holdButton = getByTestId('call-view-hold');
		const muteButton = getByTestId('call-view-mute');

		// Test that enabled buttons trigger onPress
		fireEvent.press(holdButton);
		fireEvent.press(muteButton);
		expect(toggleHold).toHaveBeenCalledTimes(1);
		expect(toggleMute).toHaveBeenCalledTimes(1);
	});

	it('should call toggleMute when mute button is pressed', () => {
		setStoreState({ callState: 'active' });
		const toggleMute = jest.fn();
		useCallStore.setState({ toggleMute });

		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		fireEvent.press(getByTestId('call-view-mute'));
		expect(toggleMute).toHaveBeenCalledTimes(1);
	});

	it('should call toggleHold when hold button is pressed', () => {
		setStoreState({ callState: 'active' });
		const toggleHold = jest.fn();
		useCallStore.setState({ toggleHold });

		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		fireEvent.press(getByTestId('call-view-hold'));
		expect(toggleHold).toHaveBeenCalledTimes(1);
	});

	it('should call toggleSpeaker when speaker button is pressed', () => {
		setStoreState({ callState: 'active' });
		const toggleSpeaker = jest.fn();
		useCallStore.setState({ toggleSpeaker });

		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		fireEvent.press(getByTestId('call-view-speaker'));
		expect(toggleSpeaker).toHaveBeenCalledTimes(1);
	});

	it('should call endCall when end button is pressed', () => {
		setStoreState({ callState: 'active' });
		const endCall = jest.fn();
		useCallStore.setState({ endCall });

		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		fireEvent.press(getByTestId('call-view-end'));
		expect(endCall).toHaveBeenCalledTimes(1);
	});

	it('should call navigateToCallRoom when message button is pressed', () => {
		setStoreState({ callState: 'active' });
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		fireEvent.press(getByTestId('call-view-message'));
		expect(mockNavigateToCallRoom).toHaveBeenCalledTimes(1);
	});

	it('should show action sheet with dialpad when dialpad button is pressed', () => {
		setStoreState({ callState: 'active' });
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		fireEvent.press(getByTestId('call-view-dialpad'));
		expect(mockShowActionSheetRef).toHaveBeenCalledWith(
			expect.objectContaining({
				children: expect.anything()
			})
		);
	});

	it('should show "Cancel" label when call is connecting', () => {
		setStoreState({ callState: 'ringing' });
		const { getByText } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		expect(getByText('Cancel')).toBeTruthy();
	});

	it('should show "End" label when call is active', () => {
		setStoreState({ callState: 'active' });
		const { getByText } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		expect(getByText('End')).toBeTruthy();
	});

	it('should render call view when call is muted and active', () => {
		setStoreState({ callState: 'active', isMuted: true });
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		expect(getByTestId('caller-info-toggle')).toBeTruthy();
		expect(getByTestId('call-view-mute')).toBeTruthy();
	});

	it('should render call view when call is muted but not yet active', () => {
		setStoreState({ callState: 'ringing', isMuted: true });
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		expect(getByTestId('caller-info-toggle')).toBeTruthy();
	});

	it('should show correct icon for speaker button when speaker is on', () => {
		setStoreState({ callState: 'active', isSpeakerOn: true });
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		// The button should be rendered (we test behavior rather than props)
		const speakerButton = getByTestId('call-view-speaker');
		expect(speakerButton).toBeTruthy();
	});

	it('should show correct icon for speaker button when speaker is off', () => {
		setStoreState({ callState: 'active', isSpeakerOn: false });
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		// The button should be rendered (we test behavior rather than props)
		const speakerButton = getByTestId('call-view-speaker');
		expect(speakerButton).toBeTruthy();
	});

	it('should render mute button correctly when muted', () => {
		setStoreState({ callState: 'active', isMuted: true });
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		// The button should be rendered (we test behavior rather than props)
		const muteButton = getByTestId('call-view-mute');
		expect(muteButton).toBeTruthy();
	});

	it('should render hold button correctly when on hold', () => {
		setStoreState({ callState: 'active', isOnHold: true });
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		// The button should be rendered (we test behavior rather than props)
		const holdButton = getByTestId('call-view-hold');
		expect(holdButton).toBeTruthy();
	});

	it('should show correct label for hold button based on state', () => {
		setStoreState({ callState: 'active', isOnHold: true });
		const { getByText } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		expect(getByText('Unhold')).toBeTruthy();
	});

	it('should show correct label for mute button based on state', () => {
		setStoreState({ callState: 'active', isMuted: true });
		const { getByText } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		expect(getByText('Unmute')).toBeTruthy();
	});

	it('should render buttons in a single row on wide layout', () => {
		mockWindowWidth = 800;
		mockWindowHeight = 800;
		setStoreState();
		const { getByTestId, queryByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);
		expect(getByTestId('call-buttons-row-0')).toBeTruthy();
		expect(queryByTestId('call-buttons-row-1')).toBeNull();
	});

	it('should render buttons in a single row in phone landscape (narrow, width > height)', () => {
		mockWindowWidth = 600;
		mockWindowHeight = 400;
		setStoreState();
		const { getByTestId, queryByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);
		expect(getByTestId('call-buttons-row-0')).toBeTruthy();
		expect(queryByTestId('call-buttons-row-1')).toBeNull();
	});
});

describe('CallView (tablet/wide layout)', () => {
	beforeEach(() => {
		mockWindowWidth = 800;
		mockWindowHeight = 800;
		useCallStore.getState().reset();
		jest.clearAllMocks();
	});

	afterAll(() => {
		mockWindowWidth = 350;
		mockWindowHeight = 800;
	});

	it('renders all six action buttons in a single row', () => {
		setStoreState();
		const { getByTestId, queryByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		expect(getByTestId('call-buttons-row-0')).toBeTruthy();
		expect(queryByTestId('call-buttons-row-1')).toBeNull();
		expect(getByTestId('call-view-speaker')).toBeTruthy();
		expect(getByTestId('call-view-hold')).toBeTruthy();
		expect(getByTestId('call-view-mute')).toBeTruthy();
		expect(getByTestId('call-view-message')).toBeTruthy();
		expect(getByTestId('call-view-end')).toBeTruthy();
		expect(getByTestId('call-view-dialpad')).toBeTruthy();
	});

	it('places every action button inside row 0', () => {
		setStoreState();
		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		const row0 = getByTestId('call-buttons-row-0');
		const ids = [
			'call-view-speaker',
			'call-view-hold',
			'call-view-mute',
			'call-view-message',
			'call-view-end',
			'call-view-dialpad'
		];
		ids.forEach(id => {
			expect(within(row0).getByTestId(id)).toBeTruthy();
		});
	});

	it('returns null when there is no call on wide layout', () => {
		useCallStore.setState({ call: null });
		const { queryByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);
		expect(queryByTestId('call-buttons-row-0')).toBeNull();
	});

	it('disables hold/mute/dialpad while connecting; speaker stays enabled on wide layout', () => {
		const toggleHold = jest.fn();
		const toggleMute = jest.fn();
		const toggleSpeaker = jest.fn();
		setStoreState({ callState: 'ringing' });
		useCallStore.setState({ toggleHold, toggleMute, toggleSpeaker });

		const { getByTestId } = render(
			<Wrapper>
				<CallView />
			</Wrapper>
		);

		fireEvent.press(getByTestId('call-view-hold'));
		fireEvent.press(getByTestId('call-view-mute'));
		fireEvent.press(getByTestId('call-view-dialpad'));
		fireEvent.press(getByTestId('call-view-speaker'));
		expect(toggleHold).not.toHaveBeenCalled();
		expect(toggleMute).not.toHaveBeenCalled();
		expect(toggleSpeaker).toHaveBeenCalled();
	});
});

describe('CallView outgoing ringback', () => {
	const originalOS = Platform.OS;

	beforeEach(() => {
		mockStartRingback.mockClear();
		mockStopRingback.mockClear();
		useCallStore.getState().reset();
	});

	afterEach(() => {
		(Platform as { OS: string }).OS = originalOS;
	});

	describe('Android', () => {
		beforeEach(() => {
			(Platform as { OS: string }).OS = 'android';
		});

		it('starts native ringback while ringing+outgoing', () => {
			setStoreState({ callState: 'ringing', direction: 'outgoing' });
			render(
				<Wrapper>
					<CallView />
				</Wrapper>
			);
			expect(mockStartRingback).toHaveBeenCalledTimes(1);
			expect(mockStopRingback).not.toHaveBeenCalled();
		});

		it('does not start ringback for incoming ringing', () => {
			setStoreState({ callState: 'ringing', direction: 'incoming' });
			render(
				<Wrapper>
					<CallView />
				</Wrapper>
			);
			expect(mockStartRingback).not.toHaveBeenCalled();
		});

		it('does not start ringback when call is active', () => {
			setStoreState({ callState: 'active', direction: 'outgoing' });
			render(
				<Wrapper>
					<CallView />
				</Wrapper>
			);
			expect(mockStartRingback).not.toHaveBeenCalled();
		});

		it('stops ringback on unmount', () => {
			setStoreState({ callState: 'ringing', direction: 'outgoing' });
			const { unmount } = render(
				<Wrapper>
					<CallView />
				</Wrapper>
			);
			expect(mockStartRingback).toHaveBeenCalledTimes(1);
			unmount();
			expect(mockStopRingback).toHaveBeenCalledTimes(1);
		});
	});

	describe('iOS', () => {
		beforeEach(() => {
			(Platform as { OS: string }).OS = 'ios';
		});

		it('does not call native ringback (expo-av Ringer handles iOS)', () => {
			setStoreState({ callState: 'ringing', direction: 'outgoing' });
			render(
				<Wrapper>
					<CallView />
				</Wrapper>
			);
			expect(mockStartRingback).not.toHaveBeenCalled();
			expect(mockStopRingback).not.toHaveBeenCalled();
		});
	});
});

generateSnapshots(stories);
