package de.effigenix.infrastructure.masterdata.web;

import de.effigenix.domain.masterdata.article.PriceModel;
import de.effigenix.domain.masterdata.Unit;
import de.effigenix.domain.usermanagement.RoleName;
import de.effigenix.infrastructure.AbstractIntegrationTest;
import de.effigenix.infrastructure.masterdata.web.dto.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;

import java.math.BigDecimal;
import java.util.Set;
import java.util.UUID;

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

/**
 * Integrationstests für ArticleController.
 *
 * Abgedeckte Testfälle: TC-ART-01 bis TC-ART-11
 */
@DisplayName("Article Controller Integration Tests")
class ArticleControllerIntegrationTest extends AbstractIntegrationTest {

    private String adminToken;
    private String viewerToken;
    private String categoryId;

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

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

        adminToken = generateToken(adminId, "art.admin", "MASTERDATA_WRITE");
        viewerToken = generateToken(viewerId, "art.viewer", "USER_READ");

        // Vorbedingung: Kategorie erstellen
        categoryId = createCategory("Obst & Gemüse");
    }

    // ==================== TC-ART-01: PIECE_FIXED → Preismodell FIXED ====================

    @Test
    @DisplayName("TC-ART-01: Artikel PIECE_FIXED erstellen → Preismodell FIXED, 201")
    void createArticle_pieceFix_returns201WithFixedPriceModel() throws Exception {
        var request = new CreateArticleRequest(
                "Äpfel Gala", "OG-001", categoryId,
                Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("1.99"));

        mockMvc.perform(post("/api/articles")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").isNotEmpty())
                .andExpect(jsonPath("$.name").value("Äpfel Gala"))
                .andExpect(jsonPath("$.articleNumber").value("OG-001"))
                .andExpect(jsonPath("$.salesUnits[0].unit").value("PIECE_FIXED"))
                .andExpect(jsonPath("$.salesUnits[0].priceModel").value("FIXED"))
                .andExpect(jsonPath("$.status").value("ACTIVE"));
    }

    // ==================== TC-ART-02: KG → WEIGHT_BASED ====================

    @Test
    @DisplayName("TC-ART-02: Artikel KG erstellen → Preismodell WEIGHT_BASED")
    void createArticle_kg_returnsWeightBasedPriceModel() throws Exception {
        var request = new CreateArticleRequest(
                "Bananen", "OG-002", categoryId,
                Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("2.49"));

        mockMvc.perform(post("/api/articles")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.salesUnits[0].unit").value("KG"))
                .andExpect(jsonPath("$.salesUnits[0].priceModel").value("WEIGHT_BASED"));
    }

    // ==================== TC-ART-03: HUNDRED_GRAM und PIECE_VARIABLE ====================

    @Test
    @DisplayName("TC-ART-03: Artikel HUNDRED_GRAM erstellen → Preismodell WEIGHT_BASED")
    void createArticle_hundredGram_returnsWeightBased() throws Exception {
        var request = new CreateArticleRequest(
                "Käse Scheiben", "MO-001", categoryId,
                Unit.HUNDRED_GRAM, PriceModel.WEIGHT_BASED, new BigDecimal("1.50"));

        mockMvc.perform(post("/api/articles")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.salesUnits[0].unit").value("HUNDRED_GRAM"))
                .andExpect(jsonPath("$.salesUnits[0].priceModel").value("WEIGHT_BASED"));
    }

    @Test
    @DisplayName("TC-ART-03: Artikel PIECE_VARIABLE erstellen → Preismodell WEIGHT_BASED")
    void createArticle_pieceVariable_returnsWeightBased() throws Exception {
        var request = new CreateArticleRequest(
                "Hähnchen ganz", "FL-001", categoryId,
                Unit.PIECE_VARIABLE, PriceModel.WEIGHT_BASED, new BigDecimal("5.99"));

        mockMvc.perform(post("/api/articles")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.salesUnits[0].unit").value("PIECE_VARIABLE"))
                .andExpect(jsonPath("$.salesUnits[0].priceModel").value("WEIGHT_BASED"));
    }

    // ==================== TC-ART-04: Doppelte Artikelnummer ====================

    @Test
    @DisplayName("TC-ART-04: Doppelte Artikelnummer → 409 Conflict")
    void createArticle_withDuplicateNumber_returns409() throws Exception {
        createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");

        var duplicate = new CreateArticleRequest(
                "Andere Äpfel", "OG-001", categoryId,
                Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("2.00"));

        mockMvc.perform(post("/api/articles")
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(duplicate)))
                .andExpect(status().isConflict());
    }

    // ==================== TC-ART-05: Deaktivieren und Aktivieren ====================

    @Test
    @DisplayName("TC-ART-05: Artikel deaktivieren → Status INACTIVE")
    void deactivateArticle_activeArticle_returnsInactive() throws Exception {
        String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");

        mockMvc.perform(post("/api/articles/{id}/deactivate", articleId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("INACTIVE"));
    }

    @Test
    @DisplayName("TC-ART-05: Artikel aktivieren → Status ACTIVE")
    void activateArticle_inactiveArticle_returnsActive() throws Exception {
        String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");

        mockMvc.perform(post("/api/articles/{id}/deactivate", articleId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk());

        mockMvc.perform(post("/api/articles/{id}/activate", articleId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("ACTIVE"));
    }

    // ==================== TC-ART-06: Filter ====================

    @Test
    @DisplayName("TC-ART-06: Nur aktive Artikel filtern → nur ACTIVE")
    void listArticles_filterByActive_returnsOnlyActive() throws Exception {
        String id = createArticle("Inaktiv Produkt", "IN-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.00");
        mockMvc.perform(post("/api/articles/{id}/deactivate", id)
                .header("Authorization", "Bearer " + adminToken)).andReturn();

        createArticle("Aktiv Produkt", "AC-001", Unit.KG, PriceModel.WEIGHT_BASED, "2.00");

        mockMvc.perform(get("/api/articles")
                        .param("status", "ACTIVE")
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE"))));
    }

    // ==================== TC-ART-07: Verkaufseinheit hinzufügen ====================

    @Test
    @DisplayName("TC-ART-07: Zweite Verkaufseinheit hinzufügen → 2 VEs vorhanden")
    void addSalesUnit_toExistingArticle_returnsTwoUnits() throws Exception {
        String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");

        var addUnit = new AddSalesUnitRequest(Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("3.50"));

        mockMvc.perform(post("/api/articles/{id}/sales-units", articleId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(addUnit)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.salesUnits", hasSize(2)));
    }

    // ==================== TC-ART-08: Doppelte Einheit ====================

    @Test
    @DisplayName("TC-ART-08: Doppelte Einheit hinzufügen → 409 Conflict")
    void addSalesUnit_duplicateUnit_returns409() throws Exception {
        String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");

        var duplicate = new AddSalesUnitRequest(Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("2.50"));

        mockMvc.perform(post("/api/articles/{id}/sales-units", articleId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(duplicate)))
                .andExpect(status().isConflict());
    }

    // ==================== TC-ART-09: Letzte VE kann nicht entfernt werden ====================

    @Test
    @DisplayName("TC-ART-09: Letzte Verkaufseinheit entfernen → 400 (Mindestanforderung)")
    void removeSalesUnit_lastUnit_returns400() throws Exception {
        String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");

        // Hole die salesUnitId
        MvcResult getResult = mockMvc.perform(get("/api/articles/{id}", articleId)
                        .header("Authorization", "Bearer " + adminToken))
                .andReturn();
        String salesUnitId = objectMapper.readTree(getResult.getResponse().getContentAsString())
                .get("salesUnits").get(0).get("id").asText();

        mockMvc.perform(delete("/api/articles/{id}/sales-units/{suId}", articleId, salesUnitId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isBadRequest());
    }

    // ==================== TC-ART-10: VE entfernen (2+ vorhanden) ====================

    @Test
    @DisplayName("TC-ART-10: Zweite Verkaufseinheit entfernen → nur erste bleibt übrig")
    void removeSalesUnit_whenTwoUnitsExist_removesCorrectUnit() throws Exception {
        String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");

        // Zweite VE hinzufügen
        var addUnit = new AddSalesUnitRequest(Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("3.50"));
        MvcResult addResult = mockMvc.perform(post("/api/articles/{id}/sales-units", articleId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(addUnit)))
                .andExpect(status().isCreated())
                .andReturn();

        // KG-VE-ID ermitteln
        String kgSalesUnitId = null;
        var salesUnits = objectMapper.readTree(addResult.getResponse().getContentAsString()).get("salesUnits");
        for (var su : salesUnits) {
            if ("KG".equals(su.get("unit").asText())) {
                kgSalesUnitId = su.get("id").asText();
            }
        }

        mockMvc.perform(delete("/api/articles/{id}/sales-units/{suId}", articleId, kgSalesUnitId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isNoContent());

        mockMvc.perform(get("/api/articles/{id}", articleId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.salesUnits", hasSize(1)))
                .andExpect(jsonPath("$.salesUnits[0].unit").value("PIECE_FIXED"));
    }

    // ==================== TC-ART-11: Lieferant zuweisen ====================

    @Test
    @DisplayName("TC-ART-11: Lieferant dem Artikel zuweisen → Lieferant in Detailansicht")
    void assignSupplier_toArticle_appearsInDetail() throws Exception {
        String articleId = createArticle("Äpfel Gala", "OG-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
        String supplierId = createSupplier("Frisch AG");

        var assignRequest = new AssignSupplierRequest(supplierId);

        mockMvc.perform(post("/api/articles/{id}/suppliers", articleId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(assignRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.supplierIds", hasItem(supplierId)));
    }

    // ==================== TC-AUTH ====================

    @Test
    @DisplayName("TC-AUTH-02: Artikel erstellen ohne MASTERDATA_WRITE → 403")
    void createArticle_withViewerToken_returns403() throws Exception {
        var request = new CreateArticleRequest(
                "Verboten", "VB-001", categoryId,
                Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("1.00"));

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

    @Test
    @DisplayName("TC-AUTH-01: Artikelliste ohne MASTERDATA_WRITE → 200")
    void listArticles_withViewerToken_returns200() throws Exception {
        mockMvc.perform(get("/api/articles")
                        .header("Authorization", "Bearer " + viewerToken))
                .andExpect(status().isOk());
    }

    @Test
    @DisplayName("Nicht existierenden Artikel abfragen → 404")
    void getArticle_nonExistent_returns404() throws Exception {
        mockMvc.perform(get("/api/articles/{id}", UUID.randomUUID())
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isNotFound());
    }

    // ==================== Verkaufseinheit-Preis aktualisieren ====================

    @Test
    @DisplayName("Verkaufseinheit-Preis aktualisieren → 200")
    void updateSalesUnitPrice_returns200() throws Exception {
        String articleId = createArticle("Preis-Test", "PT-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");

        MvcResult getResult = mockMvc.perform(get("/api/articles/{id}", articleId)
                        .header("Authorization", "Bearer " + adminToken))
                .andReturn();
        String salesUnitId = objectMapper.readTree(getResult.getResponse().getContentAsString())
                .get("salesUnits").get(0).get("id").asText();

        var priceRequest = new UpdateSalesUnitPriceRequest(new BigDecimal("3.49"));

        mockMvc.perform(put("/api/articles/{id}/sales-units/{suId}/price", articleId, salesUnitId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(priceRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.salesUnits[0].price").value(3.49));
    }

    // ==================== Lieferant entfernen ====================

    @Test
    @DisplayName("Lieferant vom Artikel entfernen → 204")
    void removeSupplier_returns204() throws Exception {
        String articleId = createArticle("Supplier-Test", "ST-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
        String supplierId = createSupplier("Entfern AG");

        // Zuweisen
        mockMvc.perform(post("/api/articles/{id}/suppliers", articleId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(new AssignSupplierRequest(supplierId))))
                .andExpect(status().isOk());

        // Entfernen
        mockMvc.perform(delete("/api/articles/{id}/suppliers/{supplierId}", articleId, supplierId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isNoContent());

        // Prüfen
        mockMvc.perform(get("/api/articles/{id}", articleId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.supplierIds", hasSize(0)));
    }

    // ==================== Artikel nach Kategorie filtern ====================

    @Test
    @DisplayName("Artikel nach categoryId filtern → nur passende zurückgegeben")
    void filterByCategory_returnsOnlyMatching() throws Exception {
        String otherCategoryId = createCategory("Milchprodukte");
        createArticle("Äpfel", "FI-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
        createArticleInCategory("Milch", "MI-001", Unit.KG, PriceModel.WEIGHT_BASED, "1.29", otherCategoryId);

        mockMvc.perform(get("/api/articles")
                        .param("categoryId", otherCategoryId)
                        .header("Authorization", "Bearer " + adminToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(1)))
                .andExpect(jsonPath("$[0].name").value("Milch"));
    }

    // ==================== Artikel aktualisieren ====================

    @Test
    @DisplayName("Artikel Name und Kategorie ändern → 200")
    void updateArticle_returns200() throws Exception {
        String articleId = createArticle("Alt Name", "UP-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
        String newCategoryId = createCategory("Neue Kategorie");

        var request = new UpdateArticleRequest("Neuer Name", newCategoryId);

        mockMvc.perform(put("/api/articles/{id}", articleId)
                        .header("Authorization", "Bearer " + adminToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("Neuer Name"));
    }

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

    private String createCategory(String name) throws Exception {
        var request = new CreateProductCategoryRequest(name, null);
        MvcResult result = mockMvc.perform(post("/api/categories")
                        .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 createArticle(String name, String number, Unit unit, PriceModel priceModel, String price) throws Exception {
        var request = new CreateArticleRequest(
                name, number, categoryId, unit, priceModel, new BigDecimal(price));
        MvcResult result = mockMvc.perform(post("/api/articles")
                        .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 createArticleInCategory(String name, String number, Unit unit, PriceModel priceModel, String price, String catId) throws Exception {
        var request = new CreateArticleRequest(
                name, number, catId, unit, priceModel, new BigDecimal(price));
        MvcResult result = mockMvc.perform(post("/api/articles")
                        .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 createSupplier(String name) throws Exception {
        var request = new CreateSupplierRequest(
                name, "+49 30 12345",
                null, null, null, null, null, null, null, null, null);
        MvcResult result = mockMvc.perform(post("/api/suppliers")
                        .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();
    }
}
