import {
  act,
  screen,
  userEvent,
  waitFor,
  within,
} from "@testing-library/react-native";
import inatjs from "inaturalistjs";
import Identification from "realmModels/Identification";
import useStore from "stores/useStore";
import factory, { makeResponse } from "tests/factory";
import faker from "tests/helpers/faker";
import { renderAppWithObservations } from "tests/helpers/render";
import setStoreStateLayout from "tests/helpers/setStoreStateLayout";
import setupUniqueRealm from "tests/helpers/uniqueRealm";
import { signIn, signOut, TEST_JWT } from "tests/helpers/user";

// We're explicitly testing navigation here so we want react-navigation
// working normally
jest.unmock( "@react-navigation/native" );

// UNIQUE REALM SETUP
const mockRealmIdentifier = __filename;
const { mockRealmModelsIndex, uniqueRealmBeforeAll, uniqueRealmAfterAll } = setupUniqueRealm(
  mockRealmIdentifier,
);
jest.mock( "realmModels/index", ( ) => mockRealmModelsIndex );
jest.mock( "providers/contexts", ( ) => {
  const originalModule = jest.requireActual( "providers/contexts" );
  return {
    __esModule: true,
    ...originalModule,
    RealmContext: {
      ...originalModule.RealmContext,
      useRealm: ( ) => global.mockRealms[mockRealmIdentifier],
      useQuery: ( ) => [],
    },
  };
} );
beforeAll( uniqueRealmBeforeAll );
afterAll( uniqueRealmAfterAll );
// /UNIQUE REALM SETUP

const initialStoreState = useStore.getState( );
beforeAll( async ( ) => {
  useStore.setState( initialStoreState, true );
  setStoreStateLayout( {
    isDefaultMode: false,
  } );
  // userEvent recommends fake timers
  jest.useFakeTimers( );
} );

const mockUser = factory( "LocalUser" );

const makeMockObservations = ( ) => ( [
  factory( "RemoteObservation", {
    _synced_at: faker.date.past( ),
    needsSync: jest.fn( ( ) => false ),
    wasSynced: jest.fn( ( ) => true ),
    // Suggestions won't load without a photo
    observationPhotos: [
      factory( "RemoteObservationPhoto" ),
      factory( "RemoteObservationPhoto" ),
    ],
    user: mockUser,
    observed_on_string: "2020-01-01",
  } ),
] );

// Mock the response from inatjs.computervision.score_image
const topSuggestion = {
  taxon: factory.states( "genus" )( "RemoteTaxon", { name: "Primum" } ),
  combined_score: 90,
};
const otherSuggestion = {
  taxon: factory( "RemoteTaxon", { name: "Alia suggestione" } ),
  combined_score: 50,
};

beforeEach( async ( ) => {
  const mockScoreImageResponse = makeResponse( [topSuggestion, otherSuggestion] );
  inatjs.computervision.score_image.mockResolvedValue( mockScoreImageResponse );
  inatjs.observations.observers.mockResolvedValue( makeResponse( ) );
  inatjs.taxa.fetch.mockResolvedValue( makeResponse( [topSuggestion.taxon] ) );
  inatjs.observations.viewedUpdates.mockResolvedValue( makeResponse( ) );
  inatjs.identifications.create.mockResolvedValue( {
    results: [factory( "RemoteIdentification", {
      taxon: topSuggestion.taxon,
      user: mockUser,
    } )],
  } );
  await signIn( mockUser, { realm: global.mockRealms[__filename] } );
} );

afterEach( ( ) => {
  inatjs.computervision.score_image.mockReset( );
  inatjs.observations.observers.mockReset( );
  inatjs.taxa.fetch.mockReset( );
  inatjs.observations.viewedUpdates.mockReset( );
  inatjs.identifications.create.mockReset( );
  signOut( { realm: global.mockRealms[__filename] } );
} );

// TODO: Fix failing TaxonSearch tests: Unable to find node on an unmounted component error

// describe( "TaxonSearch", ( ) => {
//   const mockSearchResultTaxon = factory( "RemoteTaxon" );

//   beforeEach( ( ) => {
//     inatjs.taxa.search.mockResolvedValue( makeResponse( [
//       mockSearchResultTaxon
//     ] ) );
//     inatjs.observations.observers.mockResolvedValue( makeResponse( [
//       {
//         observation_count: faker.number.int( ),
//         species_count: faker.number.int( ),
//         user: factory( "RemoteUser" )
//       }
//     ] ) );
//     inatjs.taxa.fetch.mockResolvedValue( makeResponse( [] ) );
//   } );

//   afterEach( ( ) => {
//     inatjs.taxa.search.mockReset( );
//     inatjs.observations.observers.mockReset( );
//     inatjs.taxa.fetch.mockReset( );
//   } );

//   const actor = userEvent.setup( );

//   // We need to navigate from MyObs to ObsDetails to Suggestions to TaxonSearch for all of these
//   // tests
//   async function navigateToTaxonSearchForObservation( observation ) {
//     const observationGridItem = await screen.findByTestId(
//       `MyObservations.obsGridItem.${observation.uuid}`
//     );
//     await actor.press( observationGridItem );
//     const suggestIdButton = await screen.findByText( "SUGGEST ID" );
//     await act( async ( ) => actor.press( suggestIdButton ) );
//     await screen.findByTestId(
//       `SuggestionsList.taxa.${topSuggestion.taxon.id}.checkmark`
//     );
//     const searchButton = await screen.findByText( "SEARCH FOR A TAXON" );
//     await actor.press( searchButton );
//   }

//   async function navigateToTaxonSearchForObservationViaObsEdit( observation ) {
//     const observationGridItem = await screen.findByTestId(
//       `MyObservations.obsGridItem.${observation.uuid}`
//     );
//     await actor.press( observationGridItem );
//     const editButton = await screen.findByLabelText( "Edit" );
//     await act( async ( ) => actor.press( editButton ) );
//     const addIdButton = await screen.findByText( "ADD AN ID" );
//     await actor.press( addIdButton );
//     await screen.findByTestId(
//       `SuggestionsList.taxa.${topSuggestion.taxon.id}.checkmark`
//     );
//     const searchButton = await screen.findByText( "SEARCH FOR A TAXON" );
//     await actor.press( searchButton );
//   }

//   it(
//     "should create an id with false vision attribute when reached from ObsDetails via"
//     + " Suggestions and search result chosen",
//     async ( ) => {
//       const { observations } = await setupAppWithSignedInUser( );
//       await navigateToTaxonSearchForObservation( observations[0] );
//       const searchInput = await screen.findByLabelText( "Search for a taxon" );
// We used toBeVisible here but the update to RN0.77 broke this expectation
//       expect( searchInput ).toBeOnTheScreen( );
//       await act(
//         async ( ) => actor.type(
//           searchInput,
//           "doesn't really matter since we're mocking the response"
//         )
//       );
//       const taxonResultButton = await screen.findByTestId(
//         `Search.taxa.${mockSearchResultTaxon.id}.checkmark`
//       );
//       expect( taxonResultButton ).toBeTruthy( );
//       await actor.press( taxonResultButton );
//       expect( await screen.findByText( "ACTIVITY" ) ).toBeTruthy( );
//       expect( inatjs.identifications.create ).toHaveBeenCalledWith( {
//         fields: Identification.ID_FIELDS,
//         identification: {
//           observation_id: observations[0].uuid,
//           taxon_id: mockSearchResultTaxon.id,
//           vision: false
//         }
//       }, {
//         api_token: TEST_JWT
//       } );
//     }
//   );

//   it(
//     "should update observation with false vision attribute when reached from ObsEdit"
//     + " and search result chosen",
//     async ( ) => {
//       const { observations } = await setupAppWithSignedInUser( );
//       await navigateToTaxonSearchForObservationViaObsEdit( observations[0] );
//       const searchInput = await screen.findByLabelText( "Search for a taxon" );
//       await act(
//         async ( ) => actor.type(
//           searchInput,
//           "doesn't really matter since we're mocking the response"
//         )
//       );
//       const taxonResultButton = await screen.findByTestId(
//         `Search.taxa.${mockSearchResultTaxon.id}.checkmark`
//       );
//       expect( taxonResultButton ).toBeTruthy( );
//       await actor.press( taxonResultButton );
//       const saveChangesButton = await screen.findByText( "SAVE CHANGES" );
//       expect( saveChangesButton ).toBeTruthy( );
//       await actor.press( saveChangesButton );
//       const savedObservation = global.mockRealms[__filename]
//         .objectForPrimaryKey( "Observation", observations[0].uuid );
//       expect( savedObservation ).toHaveProperty( "owners_identification_from_vision", false );
//     }
//   );
// } );

describe( "Suggestions", ( ) => {
  const actor = userEvent.setup( );

  // We need to navigate from MyObs to ObsDetails to Suggestions for all of these
  // tests
  const navigateToSuggestionsForObservation = async observation => {
    const observationGridItem = await screen.findByTestId(
      `MyObservations.obsGridItem.${observation.uuid}`,
    );
    await actor.press( observationGridItem );
    const suggestIdButton = await screen.findByText( "SUGGEST ID" );
    await act( async ( ) => actor.press( suggestIdButton ) );
  };

  const navigateToSuggestionsForObservationViaObsEdit = async observation => {
    const observationGridItem = await screen.findByTestId(
      `MyObservations.obsGridItem.${observation.uuid}`,
    );
    await actor.press( observationGridItem );
    const editButton = await screen.findByLabelText( "Edit" );
    await act( async ( ) => actor.press( editButton ) );
    const addIdButton = await screen.findByText( "IDENTIFY" );
    await actor.press( addIdButton );
  };

  const setupAppWithSignedInUser = async ( ) => {
    const observations = makeMockObservations( );
    useStore.setState( { observations } );
    await renderAppWithObservations( observations, __filename );
    return { observations };
  };

  it( "should create ident with vision=true via ObsDetails", async ( ) => {
    const { observations } = await setupAppWithSignedInUser( );
    await navigateToSuggestionsForObservation( observations[0] );
    const taxonId = topSuggestion.taxon.id;
    const topTaxonResultButton = await screen.findByTestId(
      `SuggestionsList.taxa.${taxonId}.checkmark`,
    );
    // We used toBeVisible here but the update to RN0.77 broke this expectation
    expect( topTaxonResultButton ).toBeOnTheScreen( );
    await actor.press( topTaxonResultButton );
    const activityTabBtn = await screen.findByText( /ACTIVITY/ );
    // We used toBeVisible here but the update to RN0.77 broke this expectation
    expect( activityTabBtn ).toBeOnTheScreen( );
    await actor.press( activityTabBtn );
    const activityTab = await screen.findByTestId( "ActivityTab" );
    // We used toBeVisible here but the update to RN0.77 broke this expectation
    expect( activityTab ).toBeOnTheScreen( );
    // open bottom sheet
    const bottomSheetText = await screen.findByText(
      /Would you like to suggest the following identification/,
    );
    // We used toBeVisible here but the update to RN0.77 broke this expectation
    expect( bottomSheetText ).toBeOnTheScreen( );
    const confirmButton = await screen.findByTestId( "SuggestIDSheet.cvSuggestionsButton" );
    // We used toBeVisible here but the update to RN0.77 broke this expectation
    expect( confirmButton ).toBeOnTheScreen( );
    await actor.press( confirmButton );
    // Wait for the actual identification we created to appear
    const taxonNameInIdent = await within( activityTab ).findByText( topSuggestion.taxon.name );
    // We used toBeVisible here but the update to RN0.77 broke this expectation
    expect( taxonNameInIdent ).toBeOnTheScreen( );
    expect( inatjs.identifications.create ).toHaveBeenCalledWith( {
      fields: Identification.ID_FIELDS,
      identification: {
        observation_id: observations[0].uuid,
        taxon_id: taxonId,
        vision: true,
      },
    }, {
      api_token: TEST_JWT,
    } );
  } );

  it( "should update observation with vision=true via ObsEdit", async ( ) => {
    const { observations } = await setupAppWithSignedInUser( );
    await navigateToSuggestionsForObservationViaObsEdit( observations[0] );
    const topTaxonResultButton = await screen.findByTestId(
      `SuggestionsList.taxa.${topSuggestion.taxon.id}.checkmark`,
    );
    expect( topTaxonResultButton ).toBeTruthy( );
    await actor.press( topTaxonResultButton );
    const saveChangesButton = await screen.findByText( "SAVE CHANGES" );
    expect( saveChangesButton ).toBeTruthy( );
    await actor.press( saveChangesButton );
    // Ensure we're back on MyObs
    const observationGridItem = await screen.findByTestId(
      `MyObservations.obsGridItem.${observations[0].uuid}`,
    );
    await waitFor( ( ) => {
      // We used toBeVisible here but the update to RN0.77 broke this expectation
      expect( observationGridItem ).toBeOnTheScreen( );
    }, { timeout: 3000, interval: 500 } );
    const savedObservation = global.mockRealms[__filename]
      .objectForPrimaryKey( "Observation", observations[0].uuid );
    expect( savedObservation ).toHaveProperty( "owners_identification_from_vision", true );
  } );

  it.todo( "should create an identification when accessed from Explore" );
} );
