import { act, fireEvent, render, waitFor, within } from '@testing-library/react-native'
import { FetchMock } from 'jest-fetch-mock/types'
import MockDate from 'mockdate'
import React from 'react'
import * as Keychain from 'react-native-keychain'
import SmsRetriever from 'react-native-sms-retriever'
import { Provider } from 'react-redux'
import { showError } from 'src/alert/actions'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { navigate, popToScreen } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { goToNextOnboardingScreen } from 'src/onboarding/steps'
import { sleep } from 'src/utils/sleep'
import VerificationCodeInputScreen from 'src/verify/VerificationCodeInputScreen'
import networkConfig from 'src/web3/networkConfig'
import MockedNavigator from 'test/MockedNavigator'
import { createMockStore, getMockStackScreenProps } from 'test/utils'
import { mockOnboardingProps } from 'test/values'

const mockOnboardingPropsSelector = jest.fn(() => mockOnboardingProps)
jest.mock('src/onboarding/steps', () => ({
  goToNextOnboardingScreen: jest.fn(),
  onboardingPropsSelector: () => mockOnboardingPropsSelector(),
}))

const mockFetch = fetch as FetchMock

const mockedKeychain = jest.mocked(Keychain)
mockedKeychain.getGenericPassword.mockResolvedValue({
  username: 'some username',
  password: 'someSignedMessage',
  service: 'some service',
  storage: 'some string',
})

const mockedSmsRetriever = jest.mocked(SmsRetriever)

const e164Number = '+31619123456'
const store = createMockStore({
  web3: {
    account: '0xabc',
  },
  app: {
    inviterAddress: '0xabc',
  },
})

const renderComponent = ({
  hasOnboarded = false,
}: {
  hasOnboarded?: boolean
} = {}) =>
  render(
    <Provider store={store}>
      <MockedNavigator
        component={VerificationCodeInputScreen}
        params={{
          countryCallingCode: '+31',
          e164Number,
          hasOnboarded,
        }}
      />
    </Provider>
  )

describe('VerificationCodeInputScreen', () => {
  beforeEach(() => {
    jest.clearAllMocks()
    mockFetch.resetMocks()
    store.clearActions()
    MockDate.reset()
  })

  it('displays the correct components and requests for the verification code on mount', async () => {
    mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), {
      status: 200,
    })
    const { getByText, getByTestId } = renderComponent()

    expect(getByText('phoneVerificationInput.title')).toBeTruthy()
    expect(
      getByText(`phoneVerificationInput.description, {"phoneNumber":"${e164Number}"}`)
    ).toBeTruthy()
    expect(getByText('phoneVerificationInput.help')).toBeTruthy()
    expect(getByTestId('PhoneVerificationCode')).toBeTruthy()
    expect(getByTestId('PhoneVerificationResendSmsBtn')).toBeDisabled()

    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1))
    expect(mockFetch).toHaveBeenCalledWith(`${networkConfig.verifyPhoneNumberUrl}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`,
      },
      body: '{"phoneNumber":"+31619123456","clientPlatform":"android","clientVersion":"0.0.1","clientBundleId":"org.celo.mobile.debug","inviterAddress":"0xabc"}',
    })
  })

  it('displays an error if verification code request fails', async () => {
    mockFetch.mockResponseOnce(JSON.stringify({ message: 'something went wrong' }), { status: 500 })
    renderComponent()

    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1))
    expect(store.getActions()).toEqual(
      expect.arrayContaining([showError(ErrorMessages.PHONE_NUMBER_VERIFICATION_FAILURE)])
    )
  })

  it('verifies the sms code and navigates to next onboarding screen', async () => {
    mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), {
      status: 200,
    })
    mockFetch.mockResponseOnce(JSON.stringify({ message: 'OK' }), {
      status: 200,
    })

    const { getByTestId } = renderComponent()

    await act(() => {
      fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456')
    })

    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2))
    expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`,
      },
      body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}',
    })
    expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy()

    await act(() => {
      jest.runOnlyPendingTimers()
    })
    expect(goToNextOnboardingScreen).toHaveBeenCalledWith({
      firstScreenInCurrentStep: Screens.VerificationStartScreen,
      onboardingProps: mockOnboardingProps,
    })
    expect(navigate).not.toHaveBeenCalled()
    expect(popToScreen).not.toHaveBeenCalled()
  })

  it('verifies the sms code and navigates to home if not in onboarding and no previous routes found', async () => {
    mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), {
      status: 200,
    })
    mockFetch.mockResponseOnce(JSON.stringify({ message: 'OK' }), {
      status: 200,
    })

    const { getByTestId } = renderComponent({ hasOnboarded: true })

    await act(() => {
      fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456')
    })

    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2))
    expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`,
      },
      body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}',
    })
    expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy()

    await act(() => {
      jest.runOnlyPendingTimers()
    })
    expect(navigate).toHaveBeenCalledWith(Screens.TabHome)
    expect(popToScreen).not.toHaveBeenCalled()
    expect(goToNextOnboardingScreen).not.toHaveBeenCalled()
  })

  it('verifies the sms code and navigates to previous route if not in onboarding', async () => {
    mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), {
      status: 200,
    })
    mockFetch.mockResponseOnce(JSON.stringify({ message: 'OK' }), {
      status: 200,
    })

    const screenProps = getMockStackScreenProps(Screens.VerificationCodeInputScreen, {
      countryCallingCode: '+31',
      e164Number,
      hasOnboarded: true,
    })

    jest.mocked(screenProps.navigation).getState = jest.fn(
      () =>
        ({
          routes: [
            { name: Screens.ProfileSubmenu },
            { name: Screens.VerificationStartScreen },
            { name: Screens.VerificationCodeInputScreen },
          ],
        }) as any
    )

    const { getByTestId } = render(
      <Provider store={store}>
        <VerificationCodeInputScreen {...screenProps} />
      </Provider>
    )

    await act(() => {
      fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456')
    })

    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2))
    expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`,
      },
      body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}',
    })
    expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy()

    await act(() => {
      jest.runOnlyPendingTimers()
    })
    expect(popToScreen).toHaveBeenCalledWith(Screens.ProfileSubmenu)
    expect(navigate).not.toHaveBeenCalled()
    expect(goToNextOnboardingScreen).not.toHaveBeenCalled()
  })

  it('waits for the verificationId to be captured before verifying sms', async () => {
    mockFetch.mockImplementation(async (url?: string | Request) => {
      if (url === networkConfig.verifyPhoneNumberUrl) {
        await sleep(1000) // some arbitrary network delay
        return new Response(JSON.stringify({ data: { verificationId: 'someId' } }))
      }
      return new Response(JSON.stringify({ message: 'OK' }))
    })

    const { getByTestId } = renderComponent()

    await act(() => {
      // enter the verification code before the verifyPhoneNumber fetch has resolved
      fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456')
    })

    await act(() => {
      // handle the verification code, and then increment the timer to resolve
      // the network delay
      jest.runOnlyPendingTimers()
    })

    expect(mockFetch).toHaveBeenCalledTimes(2)
    expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`,
      },
      body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}',
    })
    expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy()
  })

  it('reads the SMS code on Android automatically', async () => {
    mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), {
      status: 200,
    })
    mockFetch.mockResponseOnce(JSON.stringify({ message: 'OK' }), {
      status: 200,
    })

    const { getByTestId, getByText } = renderComponent()

    // Check that the SmsRetriever is started
    await waitFor(() => expect(mockedSmsRetriever.startSmsRetriever).toHaveBeenCalledTimes(1))
    expect(mockedSmsRetriever.addSmsListener).toHaveBeenCalledTimes(1)

    const smsListener = mockedSmsRetriever.addSmsListener.mock.calls[0][0]

    await act(() => {
      // Simulate the SMS code being received
      smsListener({ message: 'Your verification code for App is: 123456 5yaJvJcZt2P' })
    })

    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2))
    expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`,
      },
      body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}',
    })
    expect(getByText('123456')).toBeTruthy()
    expect(getByTestId('PhoneVerificationCode/CheckIcon')).toBeTruthy()

    await act(() => {
      jest.runOnlyPendingTimers()
    })
    expect(goToNextOnboardingScreen).toHaveBeenCalledWith({
      firstScreenInCurrentStep: Screens.VerificationStartScreen,
      onboardingProps: mockOnboardingProps,
    })
  })

  it('handles when phone number already verified', async () => {
    mockFetch.mockResponse(JSON.stringify({ message: 'Phone number already verified' }), {
      status: 400,
    })

    renderComponent()

    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1))
    expect(mockFetch).toHaveBeenCalledWith(`${networkConfig.verifyPhoneNumberUrl}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`,
      },
      body: '{"phoneNumber":"+31619123456","clientPlatform":"android","clientVersion":"0.0.1","clientBundleId":"org.celo.mobile.debug","inviterAddress":"0xabc"}',
    })

    await act(() => {
      jest.runOnlyPendingTimers()
    })
    await waitFor(() =>
      expect(goToNextOnboardingScreen).toHaveBeenCalledWith({
        firstScreenInCurrentStep: Screens.VerificationStartScreen,
        onboardingProps: mockOnboardingProps,
      })
    )
  })

  it('shows error in verifying sms code', async () => {
    mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), {
      status: 200,
    })
    mockFetch.mockRejectedValue(JSON.stringify({ message: 'Not OK' }))

    const { getByTestId } = renderComponent()

    await act(() => {
      fireEvent.changeText(getByTestId('PhoneVerificationCode'), '123456')
    })

    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2))
    expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifySmsCodeUrl}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`,
      },
      body: '{"phoneNumber":"+31619123456","verificationId":"someId","smsCode":"123456","clientPlatform":"android","clientVersion":"0.0.1"}',
    })
    expect(getByTestId('PhoneVerificationCode/ErrorIcon')).toBeTruthy()
    expect(store.getActions()).toEqual(
      expect.not.arrayContaining([showError(ErrorMessages.PHONE_NUMBER_VERIFICATION_FAILURE)])
    )

    await act(() => {
      jest.runOnlyPendingTimers()
    })
    expect(navigate).not.toHaveBeenCalled()
  })

  it('makes a request to resend the sms code and resets the timer', async () => {
    mockFetch.mockResponse(JSON.stringify({ data: { verificationId: 'someId' } }), {
      status: 200,
    })
    const dateNow = Date.now()
    MockDate.set(dateNow)
    const { getByTestId } = renderComponent()

    await act(() => {
      MockDate.set(dateNow + 30000) // 30 seconds, matching default resend delay time in ResendButtonWithDelay component
      jest.advanceTimersByTime(1000) // 1 second, to update the timer
    })

    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1))
    fireEvent.press(getByTestId('PhoneVerificationResendSmsBtn'))
    await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2))

    expect(mockFetch).toHaveBeenNthCalledWith(2, `${networkConfig.verifyPhoneNumberUrl}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        authorization: `${networkConfig.authHeaderIssuer} 0xabc:someSignedMessage`,
      },
      body: `{"phoneNumber":"${e164Number}","clientPlatform":"android","clientVersion":"0.0.1","clientBundleId":"org.celo.mobile.debug","inviterAddress":"0xabc"}`,
    })
    expect(getByTestId('PhoneVerificationResendSmsBtn')).toBeDisabled()
  })

  it('shows the help dialog', async () => {
    mockFetch.mockResponseOnce(JSON.stringify({ data: { verificationId: 'someId' } }), {
      status: 200,
    })
    const { getByTestId, getByText } = renderComponent()

    await act(() => {
      fireEvent.press(getByText('phoneVerificationInput.help'))
    })
    await waitFor(() => expect(getByTestId('PhoneVerificationInputHelpDialog')).toBeTruthy())
    const HelpDialog = getByTestId('PhoneVerificationInputHelpDialog')
    expect(within(HelpDialog).getByText('phoneVerificationInput.helpDialog.title')).toBeTruthy()
    expect(within(HelpDialog).getByText('phoneVerificationInput.helpDialog.body')).toBeTruthy()
  })
})
