package de.effigenix.infrastructure.inventory.web;

import de.effigenix.domain.usermanagement.RoleName;
import de.effigenix.infrastructure.AbstractIntegrationTest;
import de.effigenix.infrastructure.inventory.web.dto.CreateInventoryCountRequest;
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.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * Integrationstests für InventoryCountController.
 *
 * Abgedeckte Testfälle:
 * - US-6.1 – Inventur anlegen und Zählpositionen befüllen
 * - US-6.2 – Inventur durchführen (Ist-Mengen erfassen)
 * - US-6.3 – Inventur abschließen mit Ausgleichsbuchungen
 */
@DisplayName("InventoryCount Controller Integration Tests")
class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest {

    private String adminToken;
    private String completerToken;
    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("ic.admin", "ic.admin@test.com", Set.of(adminRoleId), "BRANCH-01");
        String completerId = createUser("ic.completer", "ic.completer@test.com", Set.of(adminRoleId), "BRANCH-01");
        String viewerId = createUser("ic.viewer", "ic.viewer@test.com", Set.of(viewerRoleId), "BRANCH-01");

        adminToken = generateToken(adminId, "ic.admin", "INVENTORY_COUNT_WRITE,INVENTORY_COUNT_READ,STOCK_WRITE,STOCK_READ,STOCK_MOVEMENT_READ");
        completerToken = generateToken(completerId, "ic.completer", "INVENTORY_COUNT_WRITE,INVENTORY_COUNT_READ,STOCK_WRITE,STOCK_READ,STOCK_MOVEMENT_READ");
        viewerToken = generateToken(viewerId, "ic.viewer", "USER_READ");

        storageLocationId = createStorageLocation();
    }

    // ==================== Inventur anlegen ====================

    @Test
    @DisplayName("Inventur anlegen → 201 mit Pflichtfeldern")
    void createInventoryCount_returns201() throws Exception {
        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());

        mockMvc.perform(post("/api/inventory/inventory-counts")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").isNotEmpty())
                .andExpect(jsonPath("$.storageLocationId").value(storageLocationId))
                .andExpect(jsonPath("$.countDate").value(LocalDate.now().toString()))
                .andExpect(jsonPath("$.status").value("OPEN"))
                .andExpect(jsonPath("$.countItems").isArray());
    }

    // ==================== Auto-Populate ====================

    @Test
    @DisplayName("Inventur mit vorhandenen Stocks → countItems werden befüllt")
    void createInventoryCount_withStocks_autoPopulates() throws Exception {
        // Stock mit Batch anlegen
        String articleId = createArticleId();
        createStockWithBatch(articleId, storageLocationId);

        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());

        mockMvc.perform(post("/api/inventory/inventory-counts")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.countItems.length()").value(1))
                .andExpect(jsonPath("$.countItems[0].articleId").value(articleId))
                .andExpect(jsonPath("$.countItems[0].expectedQuantityAmount").isNumber());
    }

    // ==================== Duplikat (aktive Inventur) ====================

    @Test
    @DisplayName("Zweite aktive Inventur für gleichen Lagerort → 409")
    void createInventoryCount_activeExists_returns409() throws Exception {
        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());

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

        // Zweite Inventur für gleichen Lagerort → 409
        mockMvc.perform(post("/api/inventory/inventory-counts")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isConflict())
                .andExpect(jsonPath("$.code").value("ACTIVE_COUNT_EXISTS"));
    }

    // ==================== GET by ID ====================

    @Test
    @DisplayName("Inventur per ID abfragen → 200")
    void getInventoryCount_returns200() throws Exception {
        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());

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

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

        mockMvc.perform(get("/api/inventory/inventory-counts/" + id)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(id))
                .andExpect(jsonPath("$.storageLocationId").value(storageLocationId));
    }

    @Test
    @DisplayName("Inventur mit unbekannter ID → 404")
    void getInventoryCount_notFound_returns404() throws Exception {
        mockMvc.perform(get("/api/inventory/inventory-counts/" + UUID.randomUUID())
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value("INVENTORY_COUNT_NOT_FOUND"));
    }

    // ==================== GET all / filtered ====================

    @Test
    @DisplayName("Alle Inventuren auflisten → 200")
    void listInventoryCounts_returns200() throws Exception {
        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());

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

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

    @Test
    @DisplayName("Inventuren nach storageLocationId filtern → 200")
    void listInventoryCounts_filterByStorageLocation_returns200() throws Exception {
        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());

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

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

    // ==================== Security ====================

    @Test
    @DisplayName("Ohne Token → 401")
    void createInventoryCount_noToken_returns401() throws Exception {
        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());

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

    @Test
    @DisplayName("Ohne INVENTORY_COUNT_WRITE Permission → 403")
    void createInventoryCount_noPermission_returns403() throws Exception {
        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());

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

    // ==================== Validation Edge Cases ====================

    @Test
    @DisplayName("Inventur mit zukünftigem Datum → 400")
    void createInventoryCount_futureDate_returns400() throws Exception {
        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().plusDays(1).toString());

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

    @Test
    @DisplayName("Inventur mit ungültigem Datumsformat → 400")
    void createInventoryCount_invalidDateFormat_returns400() throws Exception {
        String json = """
                {"storageLocationId": "%s", "countDate": "not-a-date"}
                """.formatted(storageLocationId);

        mockMvc.perform(post("/api/inventory/inventory-counts")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(json))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("INVALID_COUNT_DATE"));
    }

    @Test
    @DisplayName("Inventur ohne storageLocationId → 400")
    void createInventoryCount_missingStorageLocationId_returns400() throws Exception {
        String json = """
                {"countDate": "%s"}
                """.formatted(LocalDate.now());

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

    @Test
    @DisplayName("Inventur ohne countDate → 400")
    void createInventoryCount_missingCountDate_returns400() throws Exception {
        String json = """
                {"storageLocationId": "%s"}
                """.formatted(storageLocationId);

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

    // ==================== GET Security Edge Cases ====================

    @Test
    @DisplayName("GET by ID ohne Token → 401")
    void getInventoryCount_noToken_returns401() throws Exception {
        mockMvc.perform(get("/api/inventory/inventory-counts/" + UUID.randomUUID()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("GET by ID ohne INVENTORY_COUNT_READ Permission → 403")
    void getInventoryCount_noPermission_returns403() throws Exception {
        mockMvc.perform(get("/api/inventory/inventory-counts/" + UUID.randomUUID())
                        .header("Authorization", "Bearer " + viewerToken))
                .andExpect(status().isForbidden());
    }

    @Test
    @DisplayName("GET all ohne Token → 401")
    void listInventoryCounts_noToken_returns401() throws Exception {
        mockMvc.perform(get("/api/inventory/inventory-counts"))
                .andExpect(status().isUnauthorized());
    }

    // ==================== Leere Liste ====================

    @Test
    @DisplayName("Liste ohne Inventuren → leere Liste 200")
    void listInventoryCounts_empty_returnsEmptyList() throws Exception {
        mockMvc.perform(get("/api/inventory/inventory-counts")
                        .param("storageLocationId", UUID.randomUUID().toString())
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(0));
    }

    // ==================== US-6.2: Inventur starten ====================

    @Test
    @DisplayName("Inventur starten → 200 mit Status COUNTING")
    void startInventoryCount_returns200() throws Exception {
        String countId = createInventoryCountWithStock();

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(countId))
                .andExpect(jsonPath("$.status").value("COUNTING"));
    }

    @Test
    @DisplayName("Inventur ohne Items starten → 400 NoCountItems")
    void startInventoryCount_noItems_returns400() throws Exception {
        // Inventur ohne Stock (= ohne Items) anlegen
        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());
        var createResult = mockMvc.perform(post("/api/inventory/inventory-counts")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andReturn();

        String countId = objectMapper.readTree(createResult.getResponse().getContentAsString()).get("id").asText();

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isConflict())
                .andExpect(jsonPath("$.code").value("NO_COUNT_ITEMS"));
    }

    @Test
    @DisplayName("Bereits gestartete Inventur nochmal starten → 400")
    void startInventoryCount_alreadyCounting_returns400() throws Exception {
        String countId = createInventoryCountWithStock();

        // Erster Start
        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk());

        // Zweiter Start
        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isConflict())
                .andExpect(jsonPath("$.code").value("INVALID_STATUS_TRANSITION"));
    }

    @Test
    @DisplayName("Inventur starten mit unbekannter ID → 404")
    void startInventoryCount_notFound_returns404() throws Exception {
        mockMvc.perform(patch("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/start")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value("INVENTORY_COUNT_NOT_FOUND"));
    }

    @Test
    @DisplayName("Inventur starten ohne Token → 401")
    void startInventoryCount_noToken_returns401() throws Exception {
        mockMvc.perform(patch("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/start"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("Inventur starten ohne INVENTORY_COUNT_WRITE → 403")
    void startInventoryCount_noPermission_returns403() throws Exception {
        mockMvc.perform(patch("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/start")
                        .header("Authorization", "Bearer " + viewerToken))
                .andExpect(status().isForbidden());
    }

    // ==================== US-6.2: Ist-Mengen erfassen ====================

    @Test
    @DisplayName("Ist-Menge erfassen → 200 mit actualQuantity und Deviation")
    void recordCountItem_returns200() throws Exception {
        String countId = createInventoryCountWithStock();

        // Inventur starten
        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk());

        // Item-ID holen
        String itemId = getFirstCountItemId(countId);

        String body = """
                {"actualQuantityAmount": "20.0", "actualQuantityUnit": "KILOGRAM"}
                """;

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/items/" + itemId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("COUNTING"))
                .andExpect(jsonPath("$.countItems[0].actualQuantityAmount").isNumber())
                .andExpect(jsonPath("$.countItems[0].deviation").isNumber());
    }

    @Test
    @DisplayName("Ist-Menge erfassen in OPEN Status → 409")
    void recordCountItem_openStatus_returns409() throws Exception {
        String countId = createInventoryCountWithStock();

        // NICHT starten → bleibt OPEN
        String itemId = getFirstCountItemId(countId);

        String body = """
                {"actualQuantityAmount": "20.0", "actualQuantityUnit": "KILOGRAM"}
                """;

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/items/" + itemId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isConflict())
                .andExpect(jsonPath("$.code").value("INVALID_STATUS_TRANSITION"));
    }

    @Test
    @DisplayName("Ist-Menge mit falscher UOM → 400")
    void recordCountItem_wrongUom_returns400() throws Exception {
        String countId = createInventoryCountWithStock();

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk());

        String itemId = getFirstCountItemId(countId);

        String body = """
                {"actualQuantityAmount": "20.0", "actualQuantityUnit": "LITER"}
                """;

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/items/" + itemId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value("INVALID_QUANTITY"));
    }

    @Test
    @DisplayName("Ist-Menge für unbekanntes Item → 400")
    void recordCountItem_itemNotFound_returns400() throws Exception {
        String countId = createInventoryCountWithStock();

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk());

        String body = """
                {"actualQuantityAmount": "20.0", "actualQuantityUnit": "KILOGRAM"}
                """;

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/items/" + UUID.randomUUID())
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value("COUNT_ITEM_NOT_FOUND"));
    }

    @Test
    @DisplayName("Ist-Menge erfassen ohne Token → 401")
    void recordCountItem_noToken_returns401() throws Exception {
        String body = """
                {"actualQuantityAmount": "20.0", "actualQuantityUnit": "KILOGRAM"}
                """;

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/items/" + UUID.randomUUID())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("Ist-Menge erfassen ohne Permission → 403")
    void recordCountItem_noPermission_returns403() throws Exception {
        String body = """
                {"actualQuantityAmount": "20.0", "actualQuantityUnit": "KILOGRAM"}
                """;

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/items/" + UUID.randomUUID())
                        .header("Authorization", "Bearer " + viewerToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isForbidden());
    }

    @Test
    @DisplayName("Ist-Menge ohne Body-Felder → 400 (Validation)")
    void recordCountItem_missingFields_returns400() throws Exception {
        mockMvc.perform(patch("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/items/" + UUID.randomUUID())
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @DisplayName("Ist-Menge mit Null-Wert für actualQuantityAmount → 400")
    void recordCountItem_nullAmount_returns400() throws Exception {
        String body = """
                {"actualQuantityAmount": null, "actualQuantityUnit": "KILOGRAM"}
                """;

        mockMvc.perform(patch("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/items/" + UUID.randomUUID())
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(body))
                .andExpect(status().isBadRequest());
    }

    // ==================== US-6.3: Inventur abschließen ====================

    @Nested
    @DisplayName("US-6.3: Inventur abschließen")
    class CompleteInventoryCount {

        @Test
        @DisplayName("Inventur abschließen → 200 mit COMPLETED Status und completedBy")
        void completeInventoryCount_returns200() throws Exception {
            String countId = createAndStartCountWithRecordedItems();

            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/complete")
                            .header("Authorization", "Bearer " + completerToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.id").value(countId))
                    .andExpect(jsonPath("$.status").value("COMPLETED"))
                    .andExpect(jsonPath("$.completedBy").isNotEmpty());
        }

        @Test
        @DisplayName("Inventur abschließen erzeugt ADJUSTMENT StockMovements")
        void completeInventoryCount_createsAdjustmentMovements() throws Exception {
            String countId = createAndStartCountWithRecordedItems();

            // Abschließen
            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/complete")
                            .header("Authorization", "Bearer " + completerToken))
                    .andExpect(status().isOk());

            // StockMovements prüfen – mindestens ein ADJUSTMENT
            mockMvc.perform(get("/api/inventory/stock-movements")
                            .param("movementType", "ADJUSTMENT")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(1)))
                    .andExpect(jsonPath("$[0].movementType").value("ADJUSTMENT"));
        }

        @Test
        @DisplayName("Vier-Augen-Prinzip: gleiche Person → 409 SAME_PERSON_VIOLATION")
        void completeInventoryCount_samePerson_returns409() throws Exception {
            String countId = createAndStartCountWithRecordedItems();

            // Gleicher User, der die Inventur initiiert hat
            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/complete")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("SAME_PERSON_VIOLATION"));
        }

        @Test
        @DisplayName("Inventur mit unvollständigen Items abschließen → 409 INCOMPLETE_COUNT_ITEMS")
        void completeInventoryCount_incompleteItems_returns409() throws Exception {
            String countId = createInventoryCountWithStock();

            // Starten, aber keine Ist-Mengen erfassen
            mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk());

            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/complete")
                            .header("Authorization", "Bearer " + completerToken))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("INCOMPLETE_COUNT_ITEMS"));
        }

        @Test
        @DisplayName("OPEN Inventur abschließen → 409 INVALID_STATUS_TRANSITION")
        void completeInventoryCount_openStatus_returns409() throws Exception {
            String countId = createInventoryCountWithStock();

            // Nicht starten → bleibt OPEN
            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/complete")
                            .header("Authorization", "Bearer " + completerToken))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("INVALID_STATUS_TRANSITION"));
        }

        @Test
        @DisplayName("Unbekannte ID → 404 INVENTORY_COUNT_NOT_FOUND")
        void completeInventoryCount_notFound_returns404() throws Exception {
            mockMvc.perform(post("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/complete")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isNotFound())
                    .andExpect(jsonPath("$.code").value("INVENTORY_COUNT_NOT_FOUND"));
        }

        @Test
        @DisplayName("Ohne Token → 401")
        void completeInventoryCount_noToken_returns401() throws Exception {
            mockMvc.perform(post("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/complete"))
                    .andExpect(status().isUnauthorized());
        }

        @Test
        @DisplayName("Ohne INVENTORY_COUNT_WRITE → 403")
        void completeInventoryCount_noPermission_returns403() throws Exception {
            mockMvc.perform(post("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/complete")
                            .header("Authorization", "Bearer " + viewerToken))
                    .andExpect(status().isForbidden());
        }

        @Test
        @DisplayName("Bereits abgeschlossene Inventur nochmal abschließen → 409")
        void completeInventoryCount_alreadyCompleted_returns409() throws Exception {
            String countId = createAndStartCountWithRecordedItems();

            // Erster Abschluss
            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/complete")
                            .header("Authorization", "Bearer " + completerToken))
                    .andExpect(status().isOk());

            // Zweiter Abschluss
            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/complete")
                            .header("Authorization", "Bearer " + completerToken))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("INVALID_STATUS_TRANSITION"));
        }

        @Test
        @DisplayName("Inventur ohne Abweichung → 200, keine StockMovements erzeugt")
        void completeInventoryCount_noDeviation_noMovements() throws Exception {
            String countId = createAndStartCountWithExactItems();

            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/complete")
                            .header("Authorization", "Bearer " + completerToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.status").value("COMPLETED"));
        }

        private String createAndStartCountWithRecordedItems() throws Exception {
            String countId = createInventoryCountWithStock();

            mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk());

            String itemId = getFirstCountItemId(countId);

            // Abweichung: expected 25.0, actual 20.0
            String body = """
                    {"actualQuantityAmount": "20.0", "actualQuantityUnit": "KILOGRAM"}
                    """;
            mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/items/" + itemId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(body))
                    .andExpect(status().isOk());

            return countId;
        }

        private String createAndStartCountWithExactItems() throws Exception {
            String countId = createInventoryCountWithStock();

            mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk());

            String itemId = getFirstCountItemId(countId);

            // Keine Abweichung: expected 25.0, actual 25.0
            String body = """
                    {"actualQuantityAmount": "25.0", "actualQuantityUnit": "KILOGRAM"}
                    """;
            mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/items/" + itemId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(body))
                    .andExpect(status().isOk());

            return countId;
        }
    }

    // ==================== Inventur abbrechen (US-6.4) ====================

    @Nested
    @DisplayName("US-6.4 – Inventur abbrechen und abfragen")
    class CancelAndFilter {

        @Test
        @DisplayName("Inventur abbrechen → 200 mit Grund und Status CANCELLED")
        void cancelInventoryCount_returns200() throws Exception {
            String countId = createInventoryCountWithStock();

            String body = """
                    {"reason": "Falsche Artikel ausgewählt"}
                    """;
            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/cancel")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(body))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.status").value("CANCELLED"))
                    .andExpect(jsonPath("$.cancellationReason").value("Falsche Artikel ausgewählt"));
        }

        @Test
        @DisplayName("Inventur abbrechen ohne Grund → 400")
        void cancelInventoryCount_noReason_returns400() throws Exception {
            String countId = createInventoryCountWithStock();

            String body = """
                    {"reason": ""}
                    """;
            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/cancel")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(body))
                    .andExpect(status().isBadRequest());
        }

        @Test
        @DisplayName("Abgeschlossene Inventur abbrechen → 409")
        void cancelInventoryCount_completed_returns409() throws Exception {
            String countId = createAndCompleteCount();

            String body = """
                    {"reason": "Zu spät bemerkt"}
                    """;
            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/cancel")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(body))
                    .andExpect(status().isConflict())
                    .andExpect(jsonPath("$.code").value("INVALID_STATUS_TRANSITION"));
        }

        @Test
        @DisplayName("Filter nach Status OPEN → nur offene Inventuren")
        void listInventoryCounts_filterByStatus_returnsFiltered() throws Exception {
            // Create two counts: one open, one will be cancelled
            createInventoryCountWithStock();

            // Need a second storage location for a second count
            String storageLocationId2 = createStorageLocation();
            String articleId2 = createArticleId();
            createStockWithBatch(articleId2, storageLocationId2);

            var request2 = new CreateInventoryCountRequest(storageLocationId2, LocalDate.now().toString());
            var result2 = mockMvc.perform(post("/api/inventory/inventory-counts")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(request2)))
                    .andExpect(status().isCreated())
                    .andReturn();
            String countId2 = objectMapper.readTree(result2.getResponse().getContentAsString()).get("id").asText();

            // Cancel one
            String cancelBody = """
                    {"reason": "Nicht benötigt"}
                    """;
            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId2 + "/cancel")
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(cancelBody))
                    .andExpect(status().isOk());

            // Filter by OPEN
            mockMvc.perform(get("/api/inventory/inventory-counts?status=OPEN")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.length()").value(1))
                    .andExpect(jsonPath("$[0].status").value("OPEN"));
        }

        @Test
        @DisplayName("Filter nach ungültigem Status → 400")
        void listInventoryCounts_invalidStatus_returns400() throws Exception {
            mockMvc.perform(get("/api/inventory/inventory-counts?status=INVALID")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isBadRequest())
                    .andExpect(jsonPath("$.code").value("INVALID_STATUS"));
        }

        @Test
        @DisplayName("Inventur abbrechen ohne Token → 401")
        void cancelInventoryCount_noToken_returns401() throws Exception {
            String body = """
                    {"reason": "Test"}
                    """;
            mockMvc.perform(post("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/cancel")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(body))
                    .andExpect(status().isUnauthorized());
        }

        @Test
        @DisplayName("Inventur abbrechen ohne Berechtigung → 403")
        void cancelInventoryCount_noPermission_returns403() throws Exception {
            String body = """
                    {"reason": "Test"}
                    """;
            mockMvc.perform(post("/api/inventory/inventory-counts/" + UUID.randomUUID() + "/cancel")
                            .header("Authorization", "Bearer " + viewerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(body))
                    .andExpect(status().isForbidden());
        }

        private String createAndCompleteCount() throws Exception {
            String countId = createInventoryCountWithStock();

            mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/start")
                            .header("Authorization", "Bearer " + adminToken))
                    .andExpect(status().isOk());

            String itemId = getFirstCountItemId(countId);

            String recordBody = """
                    {"actualQuantityAmount": "25.0", "actualQuantityUnit": "KILOGRAM"}
                    """;
            mockMvc.perform(patch("/api/inventory/inventory-counts/" + countId + "/items/" + itemId)
                            .header("Authorization", "Bearer " + adminToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(recordBody))
                    .andExpect(status().isOk());

            mockMvc.perform(post("/api/inventory/inventory-counts/" + countId + "/complete")
                            .header("Authorization", "Bearer " + completerToken))
                    .andExpect(status().isOk());

            return countId;
        }
    }

    // ==================== Helpers ====================

    private String createInventoryCountWithStock() throws Exception {
        String articleId = createArticleId();
        createStockWithBatch(articleId, storageLocationId);

        var request = new CreateInventoryCountRequest(storageLocationId, LocalDate.now().toString());

        var result = mockMvc.perform(post("/api/inventory/inventory-counts")
                        .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 getFirstCountItemId(String countId) throws Exception {
        var getResult = mockMvc.perform(get("/api/inventory/inventory-counts/" + countId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andReturn();

        return objectMapper.readTree(getResult.getResponse().getContentAsString())
                .get("countItems").get(0).get("id").asText();
    }

    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 void createStockWithBatch(String articleId, String storageLocationId) throws Exception {
        String stockJson = """
                {"articleId": "%s", "storageLocationId": "%s"}
                """.formatted(articleId, storageLocationId);

        var stockResult = mockMvc.perform(post("/api/inventory/stocks")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(stockJson))
                .andExpect(status().isCreated())
                .andReturn();

        String stockId = objectMapper.readTree(stockResult.getResponse().getContentAsString()).get("id").asText();

        String batchJson = """
                {"batchId": "B-%s", "batchType": "PRODUCED", "quantityAmount": "25.0", "quantityUnit": "KILOGRAM", "expiryDate": "%s"}
                """.formatted(UUID.randomUUID().toString().substring(0, 8), LocalDate.now().plusDays(30));

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