package com.magistr.app.controller;

import com.magistr.app.config.tenant.TenantConfigWatcher;
import com.magistr.app.config.tenant.TenantConfig;
import com.magistr.app.config.tenant.TenantRoutingDataSource;
import com.magistr.app.config.tenant.TenantSecretUpdateReceipt;
import com.magistr.app.service.TenantLifecycleException;
import com.magistr.app.service.TenantLifecycleMutationResult;
import com.magistr.app.service.TenantLifecycleService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

class DatabaseControllerTest {

    private TenantLifecycleService lifecycleService;
    private TenantConfigWatcher tenantConfigWatcher;
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        TenantRoutingDataSource routingDataSource = mock(TenantRoutingDataSource.class);
        lifecycleService = mock(TenantLifecycleService.class);
        tenantConfigWatcher = mock(TenantConfigWatcher.class);
        mockMvc = standaloneSetup(new DatabaseController(
                routingDataSource,
                lifecycleService,
                tenantConfigWatcher
        )).build();
    }

    @Test
    void returnsServiceUnavailableInsteadOfFalseSuccessWhenLifecycleFails() throws Exception {
        when(lifecycleService.executeSerialized(
                org.mockito.ArgumentMatchers.<Supplier<ResponseEntity<Map<String, Object>>>>any()
        )).thenThrow(new TenantLifecycleException(
                "Не удалось выполнить миграции базы данных тенанта",
                new IllegalStateException("тестовые технические детали JDBC")
        ));

        mockMvc.perform(post("/api/database/tenants")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "name": "Тестовый университет",
                                  "domain": "test",
                                  "url": "jdbc:postgresql://db/test",
                                  "username": "user",
                                  "password": "password"
                                }
                                """))
                .andExpect(status().isServiceUnavailable())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.message")
                        .value("Не удалось выполнить миграции базы данных тенанта"))
                .andExpect(jsonPath("$.message")
                        .value(org.hamcrest.Matchers.not(org.hamcrest.Matchers.containsString("JDBC"))));

        verify(tenantConfigWatcher, never()).prepareForApiMutation();
        verify(tenantConfigWatcher, never()).registerSuccessfulMutation(any());
        verify(lifecycleService).executeSerialized(any(Supplier.class));
    }

    @Test
    void successfulAddPreparesProjectionAndRegistersPersistenceReceipt() throws Exception {
        executeSerializedActionsImmediately();
        TenantConfig saved = new TenantConfig(
                "Тестовый университет",
                "test",
                "jdbc:postgresql://db/test",
                "user",
                "password"
        );
        TenantSecretUpdateReceipt receipt = receipt(saved);
        when(lifecycleService.addOrUpdateTenantWithReceipt(any(TenantConfig.class)))
                .thenReturn(new TenantLifecycleMutationResult(saved, receipt));

        mockMvc.perform(post("/api/database/tenants")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "name": "Тестовый университет",
                                  "domain": "test",
                                  "url": "jdbc:postgresql://db/test",
                                  "username": "user",
                                  "password": "password"
                                }
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true));

        InOrder order = inOrder(lifecycleService, tenantConfigWatcher);
        order.verify(lifecycleService).executeSerialized(any(Supplier.class));
        order.verify(tenantConfigWatcher).prepareForApiMutation();
        order.verify(lifecycleService).addOrUpdateTenantWithReceipt(any(TenantConfig.class));
        order.verify(tenantConfigWatcher).registerSuccessfulMutation(receipt);
    }

    @Test
    void successfulRemovalPreparesProjectionAndRegistersPersistenceReceipt() throws Exception {
        executeSerializedActionsImmediately();
        TenantConfig removed = new TenantConfig(
                "Тестовый университет",
                "test",
                "jdbc:postgresql://db/test",
                "user",
                "password"
        );
        TenantSecretUpdateReceipt receipt = receipt();
        when(lifecycleService.removeTenantWithReceipt("test"))
                .thenReturn(new TenantLifecycleMutationResult(removed, receipt));

        mockMvc.perform(delete("/api/database/tenants/test"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true));

        InOrder order = inOrder(lifecycleService, tenantConfigWatcher);
        order.verify(lifecycleService).executeSerialized(any(Supplier.class));
        order.verify(tenantConfigWatcher).prepareForApiMutation();
        order.verify(lifecycleService).removeTenantWithReceipt("test");
        order.verify(tenantConfigWatcher).registerSuccessfulMutation(receipt);
    }

    @Test
    void projectionReadFailurePreventsAddAndReturnsServiceUnavailable() throws Exception {
        executeSerializedActionsImmediately();
        doThrow(new TenantLifecycleException(
                "Не удалось прочитать конфигурацию тенантов перед изменением",
                new IllegalStateException("тестовая ошибка чтения")
        )).when(tenantConfigWatcher).prepareForApiMutation();

        mockMvc.perform(post("/api/database/tenants")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "name": "Тестовый университет",
                                  "domain": "test",
                                  "url": "jdbc:postgresql://db/test",
                                  "username": "user",
                                  "password": "password"
                                }
                                """))
                .andExpect(status().isServiceUnavailable())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.message")
                        .value("Не удалось прочитать конфигурацию тенантов перед изменением"));

        verify(lifecycleService, never()).addOrUpdateTenantWithReceipt(any());
        verify(tenantConfigWatcher, never()).registerSuccessfulMutation(any());
    }

    @Test
    void projectionReadFailurePreventsRemovalAndReturnsServiceUnavailable() throws Exception {
        executeSerializedActionsImmediately();
        doThrow(new TenantLifecycleException(
                "Не удалось прочитать конфигурацию тенантов перед изменением",
                new IllegalStateException("тестовая ошибка чтения")
        )).when(tenantConfigWatcher).prepareForApiMutation();

        mockMvc.perform(delete("/api/database/tenants/test"))
                .andExpect(status().isServiceUnavailable())
                .andExpect(jsonPath("$.success").value(false));

        verify(lifecycleService, never()).removeTenantWithReceipt(any());
        verify(tenantConfigWatcher, never()).registerSuccessfulMutation(any());
    }

    private void executeSerializedActionsImmediately() {
        doAnswer(invocation -> invocation.<Supplier<?>>getArgument(0).get())
                .when(lifecycleService)
                .executeSerialized(org.mockito.ArgumentMatchers.<Supplier<Object>>any());
    }

    private TenantSecretUpdateReceipt receipt(TenantConfig... tenants) {
        return new TenantSecretUpdateReceipt(
                true,
                true,
                "resource-version",
                List.of(),
                List.of(tenants)
        );
    }
}
