package de.effigenix.infrastructure.production.web;

import de.effigenix.domain.usermanagement.RoleName;
import de.effigenix.infrastructure.AbstractIntegrationTest;
import de.effigenix.infrastructure.production.web.dto.CreateProductionOrderRequest;
import de.effigenix.infrastructure.usermanagement.persistence.entity.RoleEntity;
import de.effigenix.infrastructure.usermanagement.persistence.entity.UserEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;

import java.time.LocalDate;
import java.util.Set;
import java.util.UUID;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@DisplayName("ProductionOrder Controller Integration Tests")
class ProductionOrderControllerIntegrationTest extends AbstractIntegrationTest {

    private String adminToken;
    private String viewerToken;

    private static final LocalDate PLANNED_DATE = LocalDate.now().plusDays(7);

    @BeforeEach
    void setUp() throws Exception {
        RoleEntity adminRole = createRole(RoleName.ADMIN, "Admin");
        RoleEntity viewerRole = createRole(RoleName.PRODUCTION_WORKER, "Viewer");

        UserEntity admin = createUser("po.admin", "po.admin@test.com", Set.of(adminRole), "BRANCH-01");
        UserEntity viewer = createUser("po.viewer", "po.viewer@test.com", Set.of(viewerRole), "BRANCH-01");

        adminToken = generateToken(admin.getId(), "po.admin",
                "PRODUCTION_ORDER_WRITE,PRODUCTION_ORDER_READ,RECIPE_WRITE,RECIPE_READ");
        viewerToken = generateToken(viewer.getId(), "po.viewer", "USER_READ");
    }

    @Nested
    @DisplayName("POST /api/production/production-orders – Produktionsauftrag anlegen")
    class CreateProductionOrderEndpoint {

        @Test
        @DisplayName("Produktionsauftrag mit gültigen Daten → 201")
        void createOrder_withValidData_returns201() throws Exception {
            String recipeId = createActiveRecipe();

            var request = new CreateProductionOrderRequest(
                    recipeId, "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("$.id").isNotEmpty())
                    .andExpect(jsonPath("$.recipeId").value(recipeId))
                    .andExpect(jsonPath("$.status").value("PLANNED"))
                    .andExpect(jsonPath("$.plannedQuantity").value("100.000000"))
                    .andExpect(jsonPath("$.plannedQuantityUnit").value("KILOGRAM"))
                    .andExpect(jsonPath("$.plannedDate").value(PLANNED_DATE.toString()))
                    .andExpect(jsonPath("$.priority").value("NORMAL"))
                    .andExpect(jsonPath("$.notes").doesNotExist())
                    .andExpect(jsonPath("$.createdAt").isNotEmpty())
                    .andExpect(jsonPath("$.updatedAt").isNotEmpty());
        }

        @Test
        @DisplayName("Produktionsauftrag mit Notes → 201")
        void createOrder_withNotes_returns201() throws Exception {
            String recipeId = createActiveRecipe();

            var request = new CreateProductionOrderRequest(
                    recipeId, "50", "LITER", PLANNED_DATE, "URGENT", "Eilauftrag Kunde X");

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("$.priority").value("URGENT"))
                    .andExpect(jsonPath("$.notes").value("Eilauftrag Kunde X"));
        }

        @Test
        @DisplayName("PlannedQuantity 0 → 400")
        void createOrder_zeroQuantity_returns400() throws Exception {
            String recipeId = createActiveRecipe();

            var request = new CreateProductionOrderRequest(
                    recipeId, "0", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_PLANNED_QUANTITY"));
        }

        @Test
        @DisplayName("Negative Menge → 400")
        void createOrder_negativeQuantity_returns400() throws Exception {
            String recipeId = createActiveRecipe();

            var request = new CreateProductionOrderRequest(
                    recipeId, "-10", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_PLANNED_QUANTITY"));
        }

        @Test
        @DisplayName("PlannedDate in der Vergangenheit → 400")
        void createOrder_pastDate_returns400() throws Exception {
            String recipeId = createActiveRecipe();

            var request = new CreateProductionOrderRequest(
                    recipeId, "100", "KILOGRAM", LocalDate.now().minusDays(1), "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_PLANNED_DATE_IN_PAST"));
        }

        @Test
        @DisplayName("Ungültige Priority → 400")
        void createOrder_invalidPriority_returns400() throws Exception {
            String recipeId = createActiveRecipe();

            var request = new CreateProductionOrderRequest(
                    recipeId, "100", "KILOGRAM", PLANNED_DATE, "SUPER_HIGH", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_PRIORITY"));
        }

        @Test
        @DisplayName("Rezept nicht gefunden → 400")
        void createOrder_recipeNotFound_returns400() throws Exception {
            var request = new CreateProductionOrderRequest(
                    UUID.randomUUID().toString(), "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_VALIDATION_ERROR"));
        }

        @Test
        @DisplayName("Rezept nicht ACTIVE (DRAFT) → 409")
        void createOrder_recipeNotActive_returns409() throws Exception {
            String recipeId = createDraftRecipe();

            var request = new CreateProductionOrderRequest(
                    recipeId, "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_RECIPE_NOT_ACTIVE"));
        }

        @Test
        @DisplayName("recipeId leer → 400 (Bean Validation)")
        void createOrder_blankRecipeId_returns400() throws Exception {
            var request = new CreateProductionOrderRequest(
                    "", "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Ungültige Unit → 400")
        void createOrder_invalidUnit_returns400() throws Exception {
            String recipeId = createActiveRecipe();

            var request = new CreateProductionOrderRequest(
                    recipeId, "100", "INVALID_UNIT", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_PLANNED_QUANTITY"));
        }

        @Test
        @DisplayName("PlannedDate heute → 201 (Grenzwert)")
        void createOrder_todayDate_returns201() throws Exception {
            String recipeId = createActiveRecipe();

            var request = new CreateProductionOrderRequest(
                    recipeId, "100", "KILOGRAM", LocalDate.now(), "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("$.plannedDate").value(LocalDate.now().toString()));
        }

        @Test
        @DisplayName("priority leer → 400 (Bean Validation)")
        void createOrder_blankPriority_returns400() throws Exception {
            var request = new CreateProductionOrderRequest(
                    UUID.randomUUID().toString(), "100", "KILOGRAM", PLANNED_DATE, "", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("plannedDate null → 400 (Bean Validation)")
        void createOrder_nullDate_returns400() throws Exception {
            String json = """
                    {"recipeId": "%s", "plannedQuantity": "100", "plannedQuantityUnit": "KILOGRAM", "priority": "NORMAL"}
                    """.formatted(UUID.randomUUID().toString());

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(json))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Alle Priority-Werte → 201")
        void createOrder_allPriorities_returns201() throws Exception {
            for (String priority : new String[]{"LOW", "NORMAL", "HIGH", "URGENT"}) {
                String recipeId = createActiveRecipe();

                var request = new CreateProductionOrderRequest(
                        recipeId, "100", "KILOGRAM", PLANNED_DATE, priority, null);

                mockMvc.perform(post("/api/production/production-orders")
                                .header("Authorization", "Bearer " + adminToken)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(objectMapper.writeValueAsString(request)))
                        .andExpect(status().isCreated())
                        .andExpect(jsonPath("$.priority").value(priority));
            }
        }

        @Test
        @DisplayName("plannedQuantity leer → 400 (Bean Validation)")
        void createOrder_blankQuantity_returns400() throws Exception {
            var request = new CreateProductionOrderRequest(
                    UUID.randomUUID().toString(), "", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest());
        }
    }

    @Nested
    @DisplayName("Authorization")
    class AuthTests {

        @Test
        @DisplayName("Ohne PRODUCTION_ORDER_WRITE → 403")
        void createOrder_withViewerToken_returns403() throws Exception {
            var request = new CreateProductionOrderRequest(
                    UUID.randomUUID().toString(), "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .header("Authorization", "Bearer " + viewerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Ohne Token → 401")
        void createOrder_withoutToken_returns401() throws Exception {
            var request = new CreateProductionOrderRequest(
                    UUID.randomUUID().toString(), "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

            mockMvc.perform(post("/api/production/production-orders")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isUnauthorized());
        }
    }

    @Nested
    @DisplayName("POST /api/production/production-orders/{id}/release – Produktionsauftrag freigeben")
    class ReleaseProductionOrderEndpoint {

        @Test
        @DisplayName("PLANNED Order freigeben → 200, Status RELEASED")
        void releaseOrder_planned_returns200() throws Exception {
            String orderId = createPlannedOrder();

            mockMvc.perform(post("/api/production/production-orders/{id}/release", orderId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.id").value(orderId))
                    .andExpect(jsonPath("$.status").value("RELEASED"));
        }

        @Test
        @DisplayName("Bereits RELEASED Order erneut freigeben → 409")
        void releaseOrder_alreadyReleased_returns409() throws Exception {
            String orderId = createPlannedOrder();

            // First release
            mockMvc.perform(post("/api/production/production-orders/{id}/release", orderId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk());

            // Second release
            mockMvc.perform(post("/api/production/production-orders/{id}/release", orderId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_INVALID_STATUS_TRANSITION"));
        }

        @Test
        @DisplayName("Order nicht gefunden → 404")
        void releaseOrder_notFound_returns404() throws Exception {
            mockMvc.perform(post("/api/production/production-orders/{id}/release", "non-existent-id")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_NOT_FOUND"));
        }

        @Test
        @DisplayName("Ohne PRODUCTION_ORDER_WRITE → 403")
        void releaseOrder_withViewerToken_returns403() throws Exception {
            mockMvc.perform(post("/api/production/production-orders/{id}/release", "any-id")
                            .header("Authorization", "Bearer " + viewerToken))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Ohne Token → 401")
        void releaseOrder_withoutToken_returns401() throws Exception {
            mockMvc.perform(post("/api/production/production-orders/{id}/release", "any-id"))
                    .andExpect(status().isUnauthorized());
        }

        @Test
        @DisplayName("Order mit archiviertem Rezept freigeben → 409")
        void releaseOrder_archivedRecipe_returns409() throws Exception {
            String orderId = createOrderWithArchivedRecipe();

            mockMvc.perform(post("/api/production/production-orders/{id}/release", orderId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("PRODUCTION_ORDER_RECIPE_NOT_ACTIVE"));
        }
    }

    // ==================== Hilfsmethoden ====================

    private String createPlannedOrder() throws Exception {
        String recipeId = createActiveRecipe();
        var request = new CreateProductionOrderRequest(
                recipeId, "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

        var result = mockMvc.perform(post("/api/production/production-orders")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andReturn();

        return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
    }

    private String createOrderWithArchivedRecipe() throws Exception {
        // Create active recipe, create order, then archive the recipe
        String recipeId = createActiveRecipe();
        var request = new CreateProductionOrderRequest(
                recipeId, "100", "KILOGRAM", PLANNED_DATE, "NORMAL", null);

        var result = mockMvc.perform(post("/api/production/production-orders")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andReturn();

        String orderId = objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();

        // Archive the recipe
        mockMvc.perform(post("/api/recipes/{id}/archive", recipeId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk());

        return orderId;
    }

    private String createActiveRecipe() throws Exception {
        String recipeId = createDraftRecipe();

        // Add ingredient (required for activation)
        String ingredientJson = """
                {"position": 1, "articleId": "%s", "quantity": "5.5", "uom": "KILOGRAM", "substitutable": false}
                """.formatted(UUID.randomUUID().toString());

        mockMvc.perform(post("/api/recipes/{id}/ingredients", recipeId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(ingredientJson))
                .andExpect(status().isCreated());

        // Activate
        mockMvc.perform(post("/api/recipes/{id}/activate", recipeId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk());

        return recipeId;
    }

    private String createDraftRecipe() throws Exception {
        String json = """
                {
                    "name": "Test-Rezept-%s",
                    "version": 1,
                    "type": "FINISHED_PRODUCT",
                    "description": "Testrezept",
                    "yieldPercentage": 85,
                    "shelfLifeDays": 14,
                    "outputQuantity": "100",
                    "outputUom": "KILOGRAM",
                    "articleId": "article-123"
                }
                """.formatted(UUID.randomUUID().toString().substring(0, 8));

        var result = mockMvc.perform(post("/api/recipes")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isCreated())
                .andReturn();

        return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asText();
    }
}
