package de.effigenix.infrastructure.inventory.web;

import de.effigenix.domain.usermanagement.RoleName;
import de.effigenix.infrastructure.AbstractIntegrationTest;
import de.effigenix.infrastructure.inventory.web.dto.AddStockBatchRequest;
import de.effigenix.infrastructure.inventory.web.dto.CreateStockRequest;
import de.effigenix.infrastructure.inventory.web.dto.ReserveStockRequest;
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.util.Set;
import java.util.UUID;

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

/**
 * Integrationstests für StockController.
 *
 * Abgedeckte Testfälle:
 * - Story 2.1 – Bestandsposition anlegen
 * - Story 2.2 – Charge einbuchen (addBatch)
 * - Story 2.5 – Bestandsposition und Chargen abfragen
 * - Story 4.3 – Reservierung bestätigen (Material entnehmen)
 */
@DisplayName("Stock Controller Integration Tests")
class StockControllerIntegrationTest extends AbstractIntegrationTest {

    private String adminToken;
    private String viewerToken;

    private String storageLocationId;

    @BeforeEach
    void setUp() throws Exception {
        String adminRoleId = createRole(RoleName.ADMIN, "Admin");
        String viewerRoleId = createRole(RoleName.PRODUCTION_WORKER, "Viewer");

        String adminId = createUser("stock.admin", "stock.admin@test.com", Set.of(adminRoleId), "BRANCH-01");
        String viewerId = createUser("stock.viewer", "stock.viewer@test.com", Set.of(viewerRoleId), "BRANCH-01");

        adminToken = generateToken(adminId, "stock.admin", "STOCK_WRITE,STOCK_READ");
        viewerToken = generateToken(viewerId, "stock.viewer", "USER_READ");

        storageLocationId = createStorageLocation();
    }

    private String newArticleId() {
        return createArticleId();
    }

    // ==================== Bestandsposition anlegen – Pflichtfelder ====================

    @Test
    @DisplayName("Bestandsposition mit Pflichtfeldern erstellen → 201")
    void createStock_withRequiredFields_returns201() throws Exception {
        var request = new CreateStockRequest(
                newArticleId(), storageLocationId, null, null, null);

        mockMvc.perform(post("/api/inventory/stocks")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").isNotEmpty())
                .andExpect(jsonPath("$.articleId").value(request.articleId()))
                .andExpect(jsonPath("$.storageLocationId").value(storageLocationId))
                .andExpect(jsonPath("$.minimumLevel").isEmpty())
                .andExpect(jsonPath("$.minimumShelfLifeDays").isEmpty());
    }

    // ==================== Bestandsposition mit allen Feldern ====================

    @Test
    @DisplayName("Bestandsposition mit allen Feldern erstellen → 201")
    void createStock_withAllFields_returns201() throws Exception {
        var request = new CreateStockRequest(
                newArticleId(), storageLocationId, "10.5", "KILOGRAM", 30);

        mockMvc.perform(post("/api/inventory/stocks")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.articleId").value(request.articleId()))
                .andExpect(jsonPath("$.storageLocationId").value(storageLocationId))
                .andExpect(jsonPath("$.minimumLevel.amount").value(10.5))
                .andExpect(jsonPath("$.minimumLevel.unit").value("KILOGRAM"))
                .andExpect(jsonPath("$.minimumShelfLifeDays").value(30));
    }

    // ==================== Duplikat ====================

    @Test
    @DisplayName("Bestandsposition Duplikat (gleiche articleId+storageLocationId) → 409")
    void createStock_duplicate_returns409() throws Exception {
        String articleId = newArticleId();
        var request = new CreateStockRequest(articleId, storageLocationId, null, null, null);

        mockMvc.perform(post("/api/inventory/stocks")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated());

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

    // ==================== Validierungsfehler ====================

    @Test
    @DisplayName("Bestandsposition ohne articleId → 400")
    void createStock_withBlankArticleId_returns400() throws Exception {
        var request = new CreateStockRequest("", storageLocationId, null, null, null);

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

    @Test
    @DisplayName("Bestandsposition ohne storageLocationId → 400")
    void createStock_withBlankStorageLocationId_returns400() throws Exception {
        var request = new CreateStockRequest(UUID.randomUUID().toString(), "", null, null, null);

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

    @Test
    @DisplayName("Bestandsposition mit ungültigem MinimumLevel → 400")
    void createStock_withInvalidMinimumLevel_returns400() throws Exception {
        var request = new CreateStockRequest(
                UUID.randomUUID().toString(), storageLocationId, "-1", "KILOGRAM", null);

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

    @Test
    @DisplayName("Bestandsposition mit ungültiger Unit → 400")
    void createStock_withInvalidUnit_returns400() throws Exception {
        var request = new CreateStockRequest(
                UUID.randomUUID().toString(), storageLocationId, "10", "INVALID_UNIT", null);

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

    @Test
    @DisplayName("Bestandsposition mit ungültigem MinimumShelfLife → 400")
    void createStock_withInvalidMinimumShelfLife_returns400() throws Exception {
        var request = new CreateStockRequest(
                UUID.randomUUID().toString(), storageLocationId, null, null, 0);

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

    // ==================== Autorisierung ====================

    @Test
    @DisplayName("Bestandsposition erstellen ohne STOCK_WRITE → 403")
    void createStock_withViewerToken_returns403() throws Exception {
        var request = new CreateStockRequest(
                UUID.randomUUID().toString(), storageLocationId, null, null, null);

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

    @Test
    @DisplayName("Bestandsposition erstellen ohne Token → 401")
    void createStock_withoutToken_returns401() throws Exception {
        var request = new CreateStockRequest(
                UUID.randomUUID().toString(), storageLocationId, null, null, null);

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

    // ==================== Bestandsparameter ändern (updateStock) ====================

    @Nested
    @DisplayName("PUT /{id} – Bestandsparameter ändern")
    class UpdateStockEndpoint {

        @Test
        @DisplayName("MinimumLevel ändern → 200")
        void updateStock_minimumLevel_returns200() throws Exception {
            String stockId = createStock();

            mockMvc.perform(put("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumLevelAmount": "50", "minimumLevelUnit": "KILOGRAM"}
                                    """))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.id").value(stockId))
                    .andExpect(jsonPath("$.minimumLevel.amount").value(50))
                    .andExpect(jsonPath("$.minimumLevel.unit").value("KILOGRAM"));
        }

        @Test
        @DisplayName("MinimumShelfLife ändern → 200")
        void updateStock_minimumShelfLife_returns200() throws Exception {
            String stockId = createStock();

            mockMvc.perform(put("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumShelfLifeDays": 14}
                                    """))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.id").value(stockId))
                    .andExpect(jsonPath("$.minimumShelfLifeDays").value(14));
        }

        @Test
        @DisplayName("Beide Parameter gleichzeitig ändern → 200")
        void updateStock_bothParameters_returns200() throws Exception {
            String stockId = createStock();

            mockMvc.perform(put("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumLevelAmount": "25.5", "minimumLevelUnit": "LITER", "minimumShelfLifeDays": 7}
                                    """))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.minimumLevel.amount").value(25.5))
                    .andExpect(jsonPath("$.minimumLevel.unit").value("LITER"))
                    .andExpect(jsonPath("$.minimumShelfLifeDays").value(7));
        }

        @Test
        @DisplayName("Keine Parameter ändern (leerer Body) → 200 ohne Änderung")
        void updateStock_noChanges_returns200() throws Exception {
            String stockId = createStock();

            mockMvc.perform(put("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("{}"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.id").value(stockId));
        }

        @Test
        @DisplayName("Persistenz: geänderte Werte beim erneuten Laden sichtbar")
        void updateStock_persistsChanges() throws Exception {
            String stockId = createStock();

            mockMvc.perform(put("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumLevelAmount": "42", "minimumLevelUnit": "PIECE", "minimumShelfLifeDays": 21}
                                    """))
                    .andExpect(status().isOk());

            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.minimumLevel.amount").value(42))
                    .andExpect(jsonPath("$.minimumLevel.unit").value("PIECE"))
                    .andExpect(jsonPath("$.minimumShelfLifeDays").value(21));
        }

        @Test
        @DisplayName("Ungültiger MinimumLevel (negativer Amount) → 400")
        void updateStock_invalidMinimumLevel_returns400() throws Exception {
            String stockId = createStock();

            mockMvc.perform(put("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumLevelAmount": "-1", "minimumLevelUnit": "KILOGRAM"}
                                    """))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_MINIMUM_LEVEL"));
        }

        @Test
        @DisplayName("Ungültige Unit → 400")
        void updateStock_invalidUnit_returns400() throws Exception {
            String stockId = createStock();

            mockMvc.perform(put("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumLevelAmount": "10", "minimumLevelUnit": "INVALID_UNIT"}
                                    """))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_MINIMUM_LEVEL"));
        }

        @Test
        @DisplayName("Ungültige MinimumShelfLife (0) → 400")
        void updateStock_invalidShelfLife_returns400() throws Exception {
            String stockId = createStock();

            mockMvc.perform(put("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumShelfLifeDays": 0}
                                    """))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_MINIMUM_SHELF_LIFE"));
        }

        @Test
        @DisplayName("Nicht existierende Bestandsposition → 404")
        void updateStock_notFound_returns404() throws Exception {
            mockMvc.perform(put("/api/inventory/stocks/{id}", UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumLevelAmount": "10", "minimumLevelUnit": "KILOGRAM"}
                                    """))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
        }

        @Test
        @DisplayName("Bestandsparameter ändern ohne STOCK_WRITE → 403")
        void updateStock_withViewerToken_returns403() throws Exception {
            String stockId = createStock();

            mockMvc.perform(put("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + viewerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumLevelAmount": "10", "minimumLevelUnit": "KILOGRAM"}
                                    """))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Bestandsparameter ändern ohne Token → 401")
        void updateStock_withoutToken_returns401() throws Exception {
            mockMvc.perform(put("/api/inventory/stocks/{id}", UUID.randomUUID().toString())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"minimumLevelAmount": "10", "minimumLevelUnit": "KILOGRAM"}
                                    """))
                    .andExpect(status().isUnauthorized());
        }
    }

    // ==================== Charge einbuchen (addBatch) ====================

    @Nested
    @DisplayName("POST /{stockId}/batches – Charge einbuchen")
    class AddBatch {

        @Test
        @DisplayName("Charge einbuchen → 201")
        void addBatch_returns201() throws Exception {
            String stockId = createStock();
            var request = new AddStockBatchRequest("BATCH-001", "PRODUCED", "10.5", "KILOGRAM", "2026-12-31");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("$.id").isNotEmpty())
                    .andExpect(jsonPath("$.batchId").value("BATCH-001"))
                    .andExpect(jsonPath("$.batchType").value("PRODUCED"))
                    .andExpect(jsonPath("$.quantityAmount").value(10.5))
                    .andExpect(jsonPath("$.quantityUnit").value("KILOGRAM"))
                    .andExpect(jsonPath("$.expiryDate").value("2026-12-31"))
                    .andExpect(jsonPath("$.status").value("AVAILABLE"))
                    .andExpect(jsonPath("$.receivedAt").isNotEmpty());
        }

        @Test
        @DisplayName("Charge mit PURCHASED Typ → 201")
        void addBatch_purchased_returns201() throws Exception {
            String stockId = createStock();
            var request = new AddStockBatchRequest("PO-123", "PURCHASED", "5", "PIECE", "2027-01-15");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("$.batchType").value("PURCHASED"));
        }

        @Test
        @DisplayName("Doppelte BatchReference → 409")
        void addBatch_duplicate_returns409() throws Exception {
            String stockId = createStock();
            var request = new AddStockBatchRequest("BATCH-001", "PRODUCED", "10", "KILOGRAM", "2026-12-31");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated());

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("DUPLICATE_BATCH_REFERENCE"));
        }

        @Test
        @DisplayName("Ungültiger BatchType → 400")
        void addBatch_invalidBatchType_returns400() throws Exception {
            String stockId = createStock();
            var request = new AddStockBatchRequest("BATCH-001", "INVALID", "10", "KILOGRAM", "2026-12-31");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_BATCH_REFERENCE"));
        }

        @Test
        @DisplayName("Ungültige Menge → 400")
        void addBatch_invalidQuantity_returns400() throws Exception {
            String stockId = createStock();
            var request = new AddStockBatchRequest("BATCH-001", "PRODUCED", "-1", "KILOGRAM", "2026-12-31");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_QUANTITY"));
        }

        @Test
        @DisplayName("Ungültiges Ablaufdatum → 400")
        void addBatch_invalidExpiryDate_returns400() throws Exception {
            String stockId = createStock();
            var request = new AddStockBatchRequest("BATCH-001", "PRODUCED", "10", "KILOGRAM", "31.12.2026");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_EXPIRY_DATE"));
        }

        @Test
        @DisplayName("Stock nicht gefunden → 404")
        void addBatch_stockNotFound_returns404() throws Exception {
            var request = new AddStockBatchRequest("BATCH-001", "PRODUCED", "10", "KILOGRAM", "2026-12-31");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
        }

        @Test
        @DisplayName("Charge einbuchen ohne STOCK_WRITE → 403")
        void addBatch_withViewerToken_returns403() throws Exception {
            String stockId = createStock();
            var request = new AddStockBatchRequest("BATCH-001", "PRODUCED", "10", "KILOGRAM", "2026-12-31");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + viewerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Charge einbuchen ohne Token → 401")
        void addBatch_withoutToken_returns401() throws Exception {
            var request = new AddStockBatchRequest("BATCH-001", "PRODUCED", "10", "KILOGRAM", "2026-12-31");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", UUID.randomUUID().toString())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isUnauthorized());
        }
    }

    // ==================== Charge sperren (blockBatch) ====================

    @Nested
    @DisplayName("POST /{stockId}/batches/{batchId}/block – Charge sperren")
    class BlockBatch {

        @Test
        @DisplayName("Charge sperren → 200")
        void blockBatch_returns200() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId);

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": "Quality issue detected"}
                                    """))
                    .andExpect(status().isOk());
        }

        @Test
        @DisplayName("Bereits gesperrte Charge erneut sperren → 409")
        void blockBatch_alreadyBlocked_returns409() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId);

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": "Quality issue detected"}
                                    """))
                    .andExpect(status().isOk());

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": "Another reason"}
                                    """))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("BATCH_ALREADY_BLOCKED"));
        }

        @Test
        @DisplayName("Charge sperren – Batch nicht gefunden → 404")
        void blockBatch_batchNotFound_returns404() throws Exception {
            String stockId = createStock();

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": "Quality issue"}
                                    """))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("BATCH_NOT_FOUND"));
        }

        @Test
        @DisplayName("Charge sperren – Stock nicht gefunden → 404")
        void blockBatch_stockNotFound_returns404() throws Exception {
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": "Quality issue"}
                                    """))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
        }

        @Test
        @DisplayName("Charge sperren ohne reason → 400")
        void blockBatch_withoutReason_returns400() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId);

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": ""}
                                    """))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Charge sperren ohne STOCK_WRITE → 403")
        void blockBatch_withViewerToken_returns403() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId);

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
                            .header("Authorization", "Bearer " + viewerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": "Quality issue"}
                                    """))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Charge sperren ohne Token → 401")
        void blockBatch_withoutToken_returns401() throws Exception {
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": "Quality issue"}
                                    """))
                    .andExpect(status().isUnauthorized());
        }
    }

    // ==================== Charge entsperren (unblockBatch) ====================

    @Nested
    @DisplayName("POST /{stockId}/batches/{batchId}/unblock – Charge entsperren")
    class UnblockBatch {

        @Test
        @DisplayName("Gesperrte Charge entsperren → 200")
        void unblockBatch_returns200() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId);

            // Erst sperren
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": "Quality issue"}
                                    """))
                    .andExpect(status().isOk());

            // Dann entsperren
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk());
        }

        @Test
        @DisplayName("Nicht gesperrte Charge entsperren → 409")
        void unblockBatch_notBlocked_returns409() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId);

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("BATCH_NOT_BLOCKED"));
        }

        @Test
        @DisplayName("Charge entsperren – Batch nicht gefunden → 404")
        void unblockBatch_batchNotFound_returns404() throws Exception {
            String stockId = createStock();

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock", stockId, UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("BATCH_NOT_FOUND"));
        }

        @Test
        @DisplayName("Charge entsperren – Stock nicht gefunden → 404")
        void unblockBatch_stockNotFound_returns404() throws Exception {
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
        }

        @Test
        @DisplayName("Charge entsperren ohne STOCK_WRITE → 403")
        void unblockBatch_withViewerToken_returns403() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId);

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock", stockId, batchId)
                            .header("Authorization", "Bearer " + viewerToken)
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Charge entsperren ohne Token → 401")
        void unblockBatch_withoutToken_returns401() throws Exception {
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/unblock",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString())
                            .contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isUnauthorized());
        }
    }

    // ==================== Charge entnehmen (removeBatch) ====================

    @Nested
    @DisplayName("POST /{stockId}/batches/{batchId}/remove – Charge entnehmen")
    class RemoveBatch {

        @Test
        @DisplayName("Teilentnahme → 200, Restmenge korrekt")
        void removeBatch_partialQuantity_returns200() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId); // 10 KILOGRAM

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/remove", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"quantityAmount": "3", "quantityUnit": "KILOGRAM"}
                                    """))
                    .andExpect(status().isOk());

            // Restmenge prüfen
            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.totalQuantity").value(7))
                    .andExpect(jsonPath("$.availableQuantity").value(7));
        }

        @Test
        @DisplayName("Entnahme über verfügbare Menge → 409")
        void removeBatch_moreThanAvailable_returns409() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId); // 10 KILOGRAM

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/remove", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"quantityAmount": "999", "quantityUnit": "KILOGRAM"}
                                    """))
                    .andExpect(status().isConflict());
        }
    }

    // ==================== FEFO-Reservierung ====================

    @Nested
    @DisplayName("FEFO-Allokation bei Reservierung")
    class FefoReservation {

        @Test
        @DisplayName("Reservierung allokiert Charge mit frühestem Ablaufdatum zuerst (FEFO)")
        void reserve_fefoOrder_allocatesEarliestFirst() throws Exception {
            String stockId = createStock();

            // Charge mit spätem Ablaufdatum zuerst einbuchen
            var lateBatch = new AddStockBatchRequest(
                    "BATCH-LATE", "PRODUCED", "10", "KILOGRAM", "2027-12-31");
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(lateBatch)))
                    .andExpect(status().isCreated());

            // Charge mit frühem Ablaufdatum danach einbuchen
            var earlyBatch = new AddStockBatchRequest(
                    "BATCH-EARLY", "PRODUCED", "10", "KILOGRAM", "2026-06-30");
            var earlyResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(earlyBatch)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String earlyBatchId = objectMapper.readTree(earlyResult.getResponse().getContentAsString()).get("id").asText();

            // Reservierung: 5 kg – soll aus BATCH-EARLY (früheres Ablaufdatum) allokiert werden
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-FEFO", "5", "KILOGRAM", "NORMAL");
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("$.allocations.length()").value(1))
                    .andExpect(jsonPath("$.allocations[0].stockBatchId").value(earlyBatchId))
                    .andExpect(jsonPath("$.allocations[0].allocatedQuantityAmount").value(5));
        }
    }

    // ==================== Bestandsposition abfragen (getStock) ====================

    @Nested
    @DisplayName("GET /{id} – Bestandsposition abfragen")
    class GetStockById {

        @Test
        @DisplayName("Bestandsposition abfragen → 200 mit Batches und Quantities")
        void getStock_returns200WithBatchesAndQuantities() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);

            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.id").value(stockId))
                    .andExpect(jsonPath("$.batches").isArray())
                    .andExpect(jsonPath("$.batches.length()").value(1))
                    .andExpect(jsonPath("$.batches[0].batchId").isNotEmpty())
                    .andExpect(jsonPath("$.batches[0].status").value("AVAILABLE"))
                    .andExpect(jsonPath("$.totalQuantity").value(10))
                    .andExpect(jsonPath("$.quantityUnit").value("KILOGRAM"))
                    .andExpect(jsonPath("$.availableQuantity").value(10));
        }

        @Test
        @DisplayName("Bestandsposition ohne Chargen abfragen → 200 mit leeren Batches")
        void getStock_noBatches_returns200() throws Exception {
            String stockId = createStock();

            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.id").value(stockId))
                    .andExpect(jsonPath("$.batches").isArray())
                    .andExpect(jsonPath("$.batches.length()").value(0))
                    .andExpect(jsonPath("$.totalQuantity").value(0))
                    .andExpect(jsonPath("$.quantityUnit").isEmpty())
                    .andExpect(jsonPath("$.availableQuantity").value(0));
        }

        @Test
        @DisplayName("Bestandsposition mit gesperrter Charge → availableQuantity exkludiert BLOCKED")
        void getStock_withBlockedBatch_availableExcludesBlocked() throws Exception {
            String stockId = createStock();
            String batchId = addBatchToStock(stockId);

            // Charge sperren
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches/{batchId}/block", stockId, batchId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"reason": "Quality issue"}
                                    """))
                    .andExpect(status().isOk());

            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.totalQuantity").value(10))
                    .andExpect(jsonPath("$.availableQuantity").value(0));
        }

        @Test
        @DisplayName("Nicht existierende Bestandsposition → 404")
        void getStock_notFound_returns404() throws Exception {
            mockMvc.perform(get("/api/inventory/stocks/{id}", UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
        }

        @Test
        @DisplayName("Bestandsposition abfragen ohne STOCK_READ → 403")
        void getStock_withViewerToken_returns403() throws Exception {
            String stockId = createStock();

            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + viewerToken))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Bestandsposition abfragen ohne Token → 401")
        void getStock_withoutToken_returns401() throws Exception {
            mockMvc.perform(get("/api/inventory/stocks/{id}", UUID.randomUUID().toString()))
                    .andExpect(status().isUnauthorized());
        }
    }

    // ==================== Bestandspositionen auflisten (listStocks) ====================

    @Nested
    @DisplayName("GET / – Bestandspositionen auflisten")
    class ListStocksEndpoint {

        @Test
        @DisplayName("Alle Bestandspositionen auflisten → 200")
        void listStocks_returnsAll() throws Exception {
            createStock();
            String storageLocationId2 = createStorageLocation();
            createStockForLocation(storageLocationId2);

            mockMvc.perform(get("/api/inventory/stocks")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(2));
        }

        @Test
        @DisplayName("Bestandspositionen nach storageLocationId filtern → 200")
        void listStocks_filterByStorageLocationId() throws Exception {
            createStock();
            String storageLocationId2 = createStorageLocation();
            createStockForLocation(storageLocationId2);

            mockMvc.perform(get("/api/inventory/stocks")
                            .param("storageLocationId", storageLocationId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(1))
                    .andExpect(jsonPath("$[0].storageLocationId").value(storageLocationId));
        }

        @Test
        @DisplayName("Bestandspositionen nach articleId filtern → 200")
        void listStocks_filterByArticleId() throws Exception {
            String articleId = newArticleId();
            createStockForArticle(articleId);

            mockMvc.perform(get("/api/inventory/stocks")
                            .param("articleId", articleId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(1))
                    .andExpect(jsonPath("$[0].articleId").value(articleId));
        }

        @Test
        @DisplayName("Leere Liste wenn keine Stocks vorhanden → 200")
        void listStocks_empty_returns200() throws Exception {
            mockMvc.perform(get("/api/inventory/stocks")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(0));
        }

        @Test
        @DisplayName("Bestandspositionen mit Batches enthalten berechnete Quantities")
        void listStocks_withBatches_containsQuantities() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);

            mockMvc.perform(get("/api/inventory/stocks")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$[0].batches.length()").value(1))
                    .andExpect(jsonPath("$[0].totalQuantity").value(10))
                    .andExpect(jsonPath("$[0].availableQuantity").value(10));
        }

        @Test
        @DisplayName("Bestandspositionen auflisten ohne STOCK_READ → 403")
        void listStocks_withViewerToken_returns403() throws Exception {
            mockMvc.perform(get("/api/inventory/stocks")
                            .header("Authorization", "Bearer " + viewerToken))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Bestandspositionen auflisten ohne Token → 401")
        void listStocks_withoutToken_returns401() throws Exception {
            mockMvc.perform(get("/api/inventory/stocks"))
                    .andExpect(status().isUnauthorized());
        }
    }

    // ==================== Bestände unter Mindestbestand (belowMinimum) ====================

    @Nested
    @DisplayName("GET /below-minimum – Bestände unter Mindestbestand")
    class BelowMinimum {

        @Test
        @DisplayName("Stock unter Mindestbestand → wird zurückgegeben")
        void belowMinimum_returnsStockBelowMinimum() throws Exception {
            // Stock mit minimumLevel=100 KILOGRAM und einer Charge von 10 KILOGRAM → unter Minimum
            String stockId = createStockWithMinimumLevel("100", "KILOGRAM");
            addBatchToStock(stockId);  // adds 10 KILOGRAM

            mockMvc.perform(get("/api/inventory/stocks/below-minimum")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(1))
                    .andExpect(jsonPath("$[0].id").value(stockId));
        }

        @Test
        @DisplayName("Stock auf Mindestbestand → wird nicht zurückgegeben")
        void belowMinimum_excludesStockAtMinimum() throws Exception {
            // Stock mit minimumLevel=10 KILOGRAM und einer Charge von 10 KILOGRAM → auf Minimum
            String stockId = createStockWithMinimumLevel("10", "KILOGRAM");
            addBatchToStock(stockId);  // adds 10 KILOGRAM

            mockMvc.perform(get("/api/inventory/stocks/below-minimum")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(0));
        }

        @Test
        @DisplayName("Stock ohne Chargen aber mit MinimumLevel > 0 → wird zurückgegeben")
        void belowMinimum_noBatchesWithMinimumLevel() throws Exception {
            createStockWithMinimumLevel("10", "KILOGRAM");

            mockMvc.perform(get("/api/inventory/stocks/below-minimum")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(1));
        }

        @Test
        @DisplayName("Stock ohne MinimumLevel → wird nicht zurückgegeben")
        void belowMinimum_excludesStockWithoutMinimumLevel() throws Exception {
            createStock();

            mockMvc.perform(get("/api/inventory/stocks/below-minimum")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(0));
        }

        @Test
        @DisplayName("Leere Liste wenn keine Stocks vorhanden → 200")
        void belowMinimum_empty_returns200() throws Exception {
            mockMvc.perform(get("/api/inventory/stocks/below-minimum")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(0));
        }

        @Test
        @DisplayName("Ohne STOCK_READ → 403")
        void belowMinimum_withViewerToken_returns403() throws Exception {
            mockMvc.perform(get("/api/inventory/stocks/below-minimum")
                            .header("Authorization", "Bearer " + viewerToken))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Ohne Token → 401")
        void belowMinimum_withoutToken_returns401() throws Exception {
            mockMvc.perform(get("/api/inventory/stocks/below-minimum"))
                    .andExpect(status().isUnauthorized());
        }
    }

    // ==================== Bestand reservieren (FEFO) ====================

    @Nested
    @DisplayName("POST /{stockId}/reservations – Bestand reservieren")
    class ReserveStockEndpoint {

        @Test
        @DisplayName("Reservierung mit gültigen Daten → 201")
        void reserveStock_valid_returns201() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("$.id").isNotEmpty())
                    .andExpect(jsonPath("$.referenceType").value("PRODUCTION_ORDER"))
                    .andExpect(jsonPath("$.referenceId").value("PO-001"))
                    .andExpect(jsonPath("$.quantityAmount").value(5))
                    .andExpect(jsonPath("$.quantityUnit").value("KILOGRAM"))
                    .andExpect(jsonPath("$.priority").value("NORMAL"))
                    .andExpect(jsonPath("$.reservedAt").isNotEmpty())
                    .andExpect(jsonPath("$.allocations").isArray())
                    .andExpect(jsonPath("$.allocations.length()").value(1))
                    .andExpect(jsonPath("$.allocations[0].stockBatchId").isNotEmpty())
                    .andExpect(jsonPath("$.allocations[0].allocatedQuantityAmount").value(5));
        }

        @Test
        @DisplayName("SALE_ORDER mit URGENT-Priorität → 201")
        void reserveStock_saleOrderUrgent_returns201() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);
            var request = new ReserveStockRequest("SALE_ORDER", "SO-001", "3", "KILOGRAM", "URGENT");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andExpect(jsonPath("$.referenceType").value("SALE_ORDER"))
                    .andExpect(jsonPath("$.priority").value("URGENT"));
        }

        @Test
        @DisplayName("Reservierung sichtbar bei GET Stock → 200")
        void reserveStock_visibleOnGet() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated());

            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.reservations").isArray())
                    .andExpect(jsonPath("$.reservations.length()").value(1))
                    .andExpect(jsonPath("$.reservations[0].referenceId").value("PO-001"))
                    .andExpect(jsonPath("$.availableQuantity").value(5));
        }

        @Test
        @DisplayName("Ungenügender Bestand → 409")
        void reserveStock_insufficientStock_returns409() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId); // 10 KILOGRAM
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "999", "KILOGRAM", "NORMAL");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("INSUFFICIENT_STOCK"));
        }

        @Test
        @DisplayName("Ungültiger ReferenceType → 400")
        void reserveStock_invalidReferenceType_returns400() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);
            var request = new ReserveStockRequest("INVALID_TYPE", "PO-001", "5", "KILOGRAM", "NORMAL");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_REFERENCE_TYPE"));
        }

        @Test
        @DisplayName("Ungültige Priorität → 400")
        void reserveStock_invalidPriority_returns400() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "SUPER_URGENT");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_RESERVATION_PRIORITY"));
        }

        @Test
        @DisplayName("Ungültige Menge (negativ) → 400")
        void reserveStock_negativeQuantity_returns400() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "-5", "KILOGRAM", "NORMAL");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_QUANTITY"));
        }

        @Test
        @DisplayName("Stock nicht gefunden → 404")
        void reserveStock_stockNotFound_returns404() throws Exception {
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
        }

        @Test
        @DisplayName("Ohne STOCK_WRITE → 403")
        void reserveStock_withViewerToken_returns403() throws Exception {
            String stockId = createStock();
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + viewerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Ohne Token → 401")
        void reserveStock_withoutToken_returns401() throws Exception {
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", UUID.randomUUID().toString())
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isUnauthorized());
        }

        @Test
        @DisplayName("Zweite Reservierung reduziert verfügbare Menge weiter → 201")
        void reserveStock_multipleReservations_reducesAvailability() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId); // 10 KILOGRAM

            // Erste Reservierung: 6 kg
            var req1 = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "6", "KILOGRAM", "NORMAL");
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req1)))
                    .andExpect(status().isCreated());

            // Zweite Reservierung: 4 kg (genau der Rest)
            var req2 = new ReserveStockRequest("SALE_ORDER", "SO-001", "4", "KILOGRAM", "URGENT");
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req2)))
                    .andExpect(status().isCreated());

            // Dritte Reservierung: sollte fehlschlagen (nichts mehr verfügbar)
            var req3 = new ReserveStockRequest("PRODUCTION_ORDER", "PO-002", "1", "KILOGRAM", "LOW");
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req3)))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("INSUFFICIENT_STOCK"));
        }

        @Test
        @DisplayName("Leerer referenceType (Blank) → 400 (Bean Validation)")
        void reserveStock_blankReferenceType_returns400() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content("""
                                    {"referenceType": "", "referenceId": "PO-001", "quantityAmount": "5", "quantityUnit": "KILOGRAM", "priority": "NORMAL"}
                                    """))
                    .andExpect(status().isBadRequest());
        }
    }

    // ==================== Reservierung freigeben (releaseReservation) ====================

    @Nested
    @DisplayName("DELETE /{stockId}/reservations/{reservationId} – Reservierung freigeben")
    class ReleaseReservationEndpoint {

        @Test
        @DisplayName("Reservierung freigeben → 204, availableQuantity steigt")
        void releaseReservation_returns204() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId); // 10 KILOGRAM

            // Erst reservieren
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");
            var reserveResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String reservationId = objectMapper.readTree(reserveResult.getResponse().getContentAsString()).get("id").asText();

            // availableQuantity == 5
            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(jsonPath("$.availableQuantity").value(5));

            // Reservierung freigeben
            mockMvc.perform(delete("/api/inventory/stocks/{stockId}/reservations/{reservationId}", stockId, reservationId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNoContent());

            // availableQuantity == 10 (wiederhergestellt)
            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(jsonPath("$.availableQuantity").value(10))
                    .andExpect(jsonPath("$.reservations.length()").value(0));
        }

        @Test
        @DisplayName("Reservierung nicht gefunden → 404")
        void releaseReservation_notFound_returns404() throws Exception {
            String stockId = createStock();

            mockMvc.perform(delete("/api/inventory/stocks/{stockId}/reservations/{reservationId}", stockId, UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("RESERVATION_NOT_FOUND"));
        }

        @Test
        @DisplayName("Stock nicht gefunden → 404")
        void releaseReservation_stockNotFound_returns404() throws Exception {
            mockMvc.perform(delete("/api/inventory/stocks/{stockId}/reservations/{reservationId}",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
        }

        @Test
        @DisplayName("Ohne STOCK_WRITE → 403")
        void releaseReservation_withViewerToken_returns403() throws Exception {
            mockMvc.perform(delete("/api/inventory/stocks/{stockId}/reservations/{reservationId}",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + viewerToken))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Ohne Token → 401")
        void releaseReservation_withoutToken_returns401() throws Exception {
            mockMvc.perform(delete("/api/inventory/stocks/{stockId}/reservations/{reservationId}",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString()))
                    .andExpect(status().isUnauthorized());
        }

        @Test
        @DisplayName("Mehrere Reservierungen: eine freigeben, andere bleibt bestehen")
        void releaseReservation_multipleReservations_keepsOthers() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId); // 10 KILOGRAM

            // Reservierung 1: 6 kg
            var req1 = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "6", "KILOGRAM", "NORMAL");
            var res1 = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req1)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String reservationId1 = objectMapper.readTree(res1.getResponse().getContentAsString()).get("id").asText();

            // Reservierung 2: 4 kg
            var req2 = new ReserveStockRequest("SALE_ORDER", "SO-001", "4", "KILOGRAM", "URGENT");
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req2)))
                    .andExpect(status().isCreated());

            // availableQuantity == 0
            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(jsonPath("$.availableQuantity").value(0))
                    .andExpect(jsonPath("$.reservations.length()").value(2));

            // Erste Reservierung freigeben
            mockMvc.perform(delete("/api/inventory/stocks/{stockId}/reservations/{reservationId}", stockId, reservationId1)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNoContent());

            // Nur 6 kg frei, SO-001 bleibt
            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(jsonPath("$.availableQuantity").value(6))
                    .andExpect(jsonPath("$.reservations.length()").value(1))
                    .andExpect(jsonPath("$.reservations[0].referenceId").value("SO-001"));
        }

        @Test
        @DisplayName("Idempotenz: zweites DELETE auf gleiche Reservierung → 404")
        void releaseReservation_idempotent_returns404OnSecondCall() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);

            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");
            var reserveResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String reservationId = objectMapper.readTree(reserveResult.getResponse().getContentAsString()).get("id").asText();

            // Erstes DELETE → 204
            mockMvc.perform(delete("/api/inventory/stocks/{stockId}/reservations/{reservationId}", stockId, reservationId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNoContent());

            // Zweites DELETE → 404
            mockMvc.perform(delete("/api/inventory/stocks/{stockId}/reservations/{reservationId}", stockId, reservationId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("RESERVATION_NOT_FOUND"));
        }

        @Test
        @DisplayName("Reserve → Release → erneut Reserve → verfügbare Menge korrekt")
        void releaseReservation_thenReserveAgain() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId); // 10 KILOGRAM

            // Alles reservieren
            var req1 = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "10", "KILOGRAM", "NORMAL");
            var res1 = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req1)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String reservationId = objectMapper.readTree(res1.getResponse().getContentAsString()).get("id").asText();

            // Erneute Reservierung schlägt fehl
            var req2 = new ReserveStockRequest("SALE_ORDER", "SO-001", "1", "KILOGRAM", "NORMAL");
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req2)))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("INSUFFICIENT_STOCK"));

            // Freigeben
            mockMvc.perform(delete("/api/inventory/stocks/{stockId}/reservations/{reservationId}", stockId, reservationId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNoContent());

            // Jetzt geht es wieder
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req2)))
                    .andExpect(status().isCreated());

            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(jsonPath("$.availableQuantity").value(9))
                    .andExpect(jsonPath("$.reservations.length()").value(1))
                    .andExpect(jsonPath("$.reservations[0].referenceId").value("SO-001"));
        }
    }

    // ==================== Reservierung bestätigen (confirmReservation) ====================

    @Nested
    @DisplayName("POST /{stockId}/reservations/{reservationId}/confirm – Reservierung bestätigen")
    class ConfirmReservationEndpoint {

        @Test
        @DisplayName("Reservierung bestätigen → 200, Menge abgezogen, Reservation entfernt")
        void confirmReservation_returns200() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId); // 10 KILOGRAM

            // Reservieren: 5 kg
            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");
            var reserveResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String reservationId = objectMapper.readTree(reserveResult.getResponse().getContentAsString()).get("id").asText();

            // Bestätigen
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNoContent());

            // Prüfen: Reservation entfernt, Menge physisch abgezogen
            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(jsonPath("$.reservations.length()").value(0))
                    .andExpect(jsonPath("$.batches[0].quantityAmount").value(5))
                    .andExpect(jsonPath("$.availableQuantity").value(5));
        }

        @Test
        @DisplayName("Bestätigung der gesamten Charge → Batch bleibt mit Menge 0")
        void confirmReservation_fullBatch_keepsBatchWithZeroQuantity() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId); // 10 KILOGRAM

            // Alles reservieren
            var request = new ReserveStockRequest("SALE_ORDER", "SO-001", "10", "KILOGRAM", "NORMAL");
            var reserveResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String reservationId = objectMapper.readTree(reserveResult.getResponse().getContentAsString()).get("id").asText();

            // Bestätigen
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNoContent());

            // Batch bleibt mit Menge 0 (FK von stock_movements erfordert Persistenz)
            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(jsonPath("$.reservations.length()").value(0))
                    .andExpect(jsonPath("$.batches.length()").value(1))
                    .andExpect(jsonPath("$.batches[0].quantityAmount").value(0))
                    .andExpect(jsonPath("$.availableQuantity").value(0));
        }

        @Test
        @DisplayName("Reservierung nicht gefunden → 404")
        void confirmReservation_notFound_returns404() throws Exception {
            String stockId = createStock();

            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm",
                            stockId, UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("RESERVATION_NOT_FOUND"));
        }

        @Test
        @DisplayName("Stock nicht gefunden → 404")
        void confirmReservation_stockNotFound_returns404() throws Exception {
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("STOCK_NOT_FOUND"));
        }

        @Test
        @DisplayName("Ohne STOCK_WRITE → 403")
        void confirmReservation_withViewerToken_returns403() throws Exception {
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString())
                            .header("Authorization", "Bearer " + viewerToken))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Ohne Token → 401")
        void confirmReservation_withoutToken_returns401() throws Exception {
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm",
                            UUID.randomUUID().toString(), UUID.randomUUID().toString()))
                    .andExpect(status().isUnauthorized());
        }

        @Test
        @DisplayName("Idempotenz: zweites Confirm auf gleiche Reservierung → 404")
        void confirmReservation_idempotent_returns404OnSecondCall() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId);

            var request = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "5", "KILOGRAM", "NORMAL");
            var reserveResult = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String reservationId = objectMapper.readTree(reserveResult.getResponse().getContentAsString()).get("id").asText();

            // Erstes Confirm → 204
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNoContent());

            // Zweites Confirm → 404
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("RESERVATION_NOT_FOUND"));
        }

        @Test
        @DisplayName("Mehrere Reservierungen: eine bestätigen, andere bleibt bestehen")
        void confirmReservation_multipleReservations_keepsOthers() throws Exception {
            String stockId = createStock();
            addBatchToStock(stockId); // 10 KILOGRAM

            // Reservierung 1: 4 kg
            var req1 = new ReserveStockRequest("PRODUCTION_ORDER", "PO-001", "4", "KILOGRAM", "NORMAL");
            var res1 = mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req1)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String reservationId1 = objectMapper.readTree(res1.getResponse().getContentAsString()).get("id").asText();

            // Reservierung 2: 3 kg
            var req2 = new ReserveStockRequest("SALE_ORDER", "SO-001", "3", "KILOGRAM", "URGENT");
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations", stockId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(req2)))
                    .andExpect(status().isCreated());

            // Erste Reservierung bestätigen
            mockMvc.perform(post("/api/inventory/stocks/{stockId}/reservations/{reservationId}/confirm", stockId, reservationId1)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNoContent());

            // Batch: 10 - 4 = 6 kg, SO-001 bleibt
            mockMvc.perform(get("/api/inventory/stocks/{id}", stockId)
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(jsonPath("$.batches[0].quantityAmount").value(6))
                    .andExpect(jsonPath("$.reservations.length()").value(1))
                    .andExpect(jsonPath("$.reservations[0].referenceId").value("SO-001"))
                    .andExpect(jsonPath("$.availableQuantity").value(3));
        }
    }

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

    private String createStorageLocation() throws Exception {
        String json = """
                {"name": "Testlager-%s", "storageType": "DRY_STORAGE"}
                """.formatted(UUID.randomUUID().toString().substring(0, 8));

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

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

    private String addBatchToStock(String stockId) throws Exception {
        var request = new AddStockBatchRequest(
                "BATCH-" + UUID.randomUUID().toString().substring(0, 8),
                "PRODUCED", "10", "KILOGRAM", "2026-12-31");

        var result = mockMvc.perform(post("/api/inventory/stocks/{stockId}/batches", stockId)
                        .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 createStock() throws Exception {
        var request = new CreateStockRequest(
                newArticleId(), storageLocationId, null, null, null);

        var result = mockMvc.perform(post("/api/inventory/stocks")
                        .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 createStockForLocation(String locationId) throws Exception {
        var request = new CreateStockRequest(
                newArticleId(), locationId, null, null, null);

        var result = mockMvc.perform(post("/api/inventory/stocks")
                        .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 createStockWithMinimumLevel(String minimumAmount, String unit) throws Exception {
        var request = new CreateStockRequest(
                newArticleId(), storageLocationId, minimumAmount, unit, null);

        var result = mockMvc.perform(post("/api/inventory/stocks")
                        .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 createStockForArticle(String articleId) throws Exception {
        var request = new CreateStockRequest(
                articleId, storageLocationId, null, null, null);

        var result = mockMvc.perform(post("/api/inventory/stocks")
                        .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();
    }
}
