/*
 * Copyright (c) 2019-2020 "Neo4j,"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.neo4j.springframework.data.core;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.neo4j.driver.Driver;
import org.neo4j.driver.Session;
import org.neo4j.driver.Transaction;
import org.neo4j.driver.TransactionConfig;
import org.neo4j.driver.SessionConfig;
import org.neo4j.driver.reactive.RxSession;
import org.neo4j.driver.reactive.RxTransaction;
import org.neo4j.driver.types.TypeSystem;
import org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * Ensure correct behaviour of both imperative and reactive clients in and outside Springs transaction management.
 *
 * @author Michael J. Simons
 */
@ExtendWith(MockitoExtension.class)
class TransactionHandlingTest {

	@Mock
	private Driver driver;

	@Mock
	private Session session;

	@Mock
	private TypeSystem typeSystem;

	@BeforeEach
	void prepareMocks() {

		when(driver.defaultTypeSystem()).thenReturn(typeSystem);
	}

	@AfterEach
	void verifyTypeSystemOnSession() {

		verify(driver).defaultTypeSystem();
	}

	@Nested
	class Neo4jClientTest {

		@Mock
		private Transaction transaction;

		@Nested
		class AutoCloseableQueryRunnerHandlerTest {

			@Test
			void shouldCallCloseOnSession() {

				ArgumentCaptor<SessionConfig> configArgumentCaptor = ArgumentCaptor.forClass(SessionConfig.class);

				when(driver.session(any(SessionConfig.class))).thenReturn(session);

				// Make template acquire session
				DefaultNeo4jClient neo4jClient = new DefaultNeo4jClient(driver);
				try (DefaultNeo4jClient.AutoCloseableQueryRunner s = neo4jClient.getQueryRunner("aDatabase")) {
					s.run("MATCH (n) RETURN n");
				}

				verify(driver).session(configArgumentCaptor.capture());
				SessionConfig sessionConfig = configArgumentCaptor.getValue();
				assertThat(sessionConfig.database()).isPresent().contains("aDatabase");

				verify(session).run(any(String.class));
				verify(session).close();

				verifyNoMoreInteractions(driver, session, transaction);
			}

			@Test
			void shouldNotInvokeCloseOnTransaction() {

				AtomicBoolean transactionIsOpen = new AtomicBoolean(true);

				when(driver.session(any(SessionConfig.class))).thenReturn(session);
				when(session.isOpen()).thenReturn(true);
				when(session.beginTransaction(any(TransactionConfig.class))).thenReturn(transaction);
				// Mock closing of the transaction
				doAnswer(invocation -> {
					transactionIsOpen.set(false);
					return null;
				}).when(transaction).close();
				when(transaction.isOpen()).thenAnswer(invocation -> transactionIsOpen.get());

				Neo4jTransactionManager txManager = new Neo4jTransactionManager(driver);
				TransactionTemplate txTemplate = new TransactionTemplate(txManager);

				DefaultNeo4jClient neo4jClient = new DefaultNeo4jClient(driver);
				txTemplate.execute(tx -> {
					try (DefaultNeo4jClient.AutoCloseableQueryRunner s = neo4jClient.getQueryRunner(null)) {
						s.run("MATCH (n) RETURN n");
					}
					return null;
				});

				verify(transaction, times(2)).isOpen();
				verify(transaction).run(anyString());
				// Called by the transaction manager
				verify(transaction).commit();
				verify(transaction).close();
				verify(session).isOpen();
				verify(session).lastBookmark();
				verify(session).close();
				verifyNoMoreInteractions(driver, session, transaction);
			}
		}
	}

	@Nested
	class ReactiveNeo4jClientTest {

		@Mock
		private RxSession session;

		@Mock
		private RxTransaction transaction;

		@Test
		void shouldNotOpenTransactionsWithoutSubscription() {
			DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver);
			neo4jClient.query("RETURN 1").in("aDatabase").fetch().one();

			verify(driver, never()).rxSession(any(SessionConfig.class));
			verifyNoMoreInteractions(driver, session);
		}

		@Test
		void shouldCloseUnmanagedSessionOnComplete() {

			when(driver.rxSession(any(SessionConfig.class))).thenReturn(session);
			when(session.beginTransaction()).thenReturn(Mono.just(transaction));
			when(transaction.commit()).thenReturn(Mono.empty());
			when(session.close()).thenReturn(Mono.empty());

			DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver);

			Mono<String> sequence = neo4jClient.doInQueryRunnerForMono("aDatabase", tx -> Mono.just("1"));

			StepVerifier.create(sequence)
				.expectNext("1")
				.verifyComplete();

			verify(driver).rxSession(any(SessionConfig.class));
			verify(session).beginTransaction();
			verify(transaction).commit();
			verify(transaction).rollback();
			verify(session).close();
			verifyNoMoreInteractions(driver, session, transaction);
		}

		@Test
		void shouldCloseUnmanagedSessionOnError() {

			when(driver.rxSession(any(SessionConfig.class))).thenReturn(session);
			when(session.beginTransaction()).thenReturn(Mono.just(transaction));
			when(transaction.rollback()).thenReturn(Mono.empty());
			when(session.close()).thenReturn(Mono.empty());

			DefaultReactiveNeo4jClient neo4jClient = new DefaultReactiveNeo4jClient(driver);

			Mono<String> sequence = neo4jClient
				.doInQueryRunnerForMono("aDatabase", tx -> Mono.error(new SomeException()));

			StepVerifier.create(sequence)
				.expectError(SomeException.class)
				.verify();

			verify(driver).rxSession(any(SessionConfig.class));
			verify(session).beginTransaction();
			verify(transaction).commit();
			verify(transaction).rollback();
			verify(session).close();
			verifyNoMoreInteractions(driver, session, transaction);
		}
	}

	private static class SomeException extends RuntimeException {
	}
}