package com.codebyte.api.item;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
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.patch;
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.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.codebyte.api.support.AbstractPostgresIT;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.transaction.annotation.Transactional;

/**
 * End-to-end HTTP tests for the Item API, asserting status, body, ProblemDetail codes and headers.
 */
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@org.springframework.security.test.context.support.WithMockUser(roles = "USER")
class ItemControllerIT extends AbstractPostgresIT {

    @Autowired private MockMvc mockMvc;
    @Autowired private ObjectMapper objectMapper;

    private JsonNode createItem(String name, String description) throws Exception {
        String body =
                """
                {"name":"%s","description":"%s"}
                """
                        .formatted(name, description);
        MvcResult result =
                mockMvc.perform(
                                post("/api/v1/items")
                                        .contentType(MediaType.APPLICATION_JSON)
                                        .content(body))
                        .andExpect(status().isCreated())
                        .andReturn();
        return objectMapper.readTree(result.getResponse().getContentAsString());
    }

    @Test
    void should_create_item_and_return_location_header() throws Exception {
        String body = "{\"name\":\"Widget\",\"description\":\"a widget\"}";

        mockMvc.perform(post("/api/v1/items").contentType(MediaType.APPLICATION_JSON).content(body))
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", containsString("/api/v1/items/")))
                .andExpect(jsonPath("$.id").exists())
                .andExpect(jsonPath("$.name", is("Widget")))
                .andExpect(jsonPath("$.version", is(0)));
    }

    @Test
    void should_return_conflict_when_creating_duplicate_name() throws Exception {
        createItem("Widget", "first");
        String body = "{\"name\":\"widget\",\"description\":\"second\"}";

        mockMvc.perform(post("/api/v1/items").contentType(MediaType.APPLICATION_JSON).content(body))
                .andExpect(status().isConflict())
                .andExpect(jsonPath("$.code", is("ITEM_NAME_TAKEN")));
    }

    @Test
    void should_return_bad_request_with_code_when_name_blank() throws Exception {
        String body = "{\"name\":\"  \",\"description\":\"x\"}";

        mockMvc.perform(post("/api/v1/items").contentType(MediaType.APPLICATION_JSON).content(body))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code", is("VALIDATION_FAILED")))
                .andExpect(jsonPath("$.errors", hasSize(1)));
    }

    @Test
    void should_return_bad_request_when_body_malformed() throws Exception {
        mockMvc.perform(
                        post("/api/v1/items")
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{not-json"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code", is("MALFORMED_REQUEST")));
    }

    @Test
    void should_get_item_by_id() throws Exception {
        String id = createItem("Widget", "desc").get("id").asText();

        mockMvc.perform(get("/api/v1/items/{id}", id))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("Widget")));
    }

    @Test
    void should_return_not_found_with_code_for_missing_item() throws Exception {
        mockMvc.perform(get("/api/v1/items/{id}", "00000000-0000-0000-0000-000000000000"))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code", is("ITEM_NOT_FOUND")));
    }

    @Test
    void should_filter_list_by_query() throws Exception {
        createItem("Alpha widget", "one");
        createItem("Beta gadget", "two");
        createItem("Gamma sprocket", "three");

        mockMvc.perform(get("/api/v1/items").param("q", "widget"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content", hasSize(1)))
                .andExpect(jsonPath("$.content[0].name", is("Alpha widget")))
                .andExpect(jsonPath("$.totalElements", is(1)));
    }

    @Test
    void should_paginate_list() throws Exception {
        createItem("Item A", "a");
        createItem("Item B", "b");
        createItem("Item C", "c");

        mockMvc.perform(get("/api/v1/items").param("page", "0").param("size", "2"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content", hasSize(2)))
                .andExpect(jsonPath("$.size", is(2)))
                .andExpect(jsonPath("$.totalElements", is(3)))
                .andExpect(jsonPath("$.totalPages", is(2)))
                .andExpect(jsonPath("$.first", is(true)))
                .andExpect(jsonPath("$.last", is(false)));
    }

    @Test
    void should_replace_item_when_version_matches() throws Exception {
        String id = createItem("Widget", "desc").get("id").asText();
        String body = "{\"name\":\"Widget v2\",\"description\":\"updated\",\"version\":0}";

        mockMvc.perform(
                        put("/api/v1/items/{id}", id)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(body))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("Widget v2")))
                .andExpect(jsonPath("$.version", is(1)));
    }

    @Test
    void should_return_conflict_when_replacing_with_stale_version() throws Exception {
        String id = createItem("Widget", "desc").get("id").asText();
        String body = "{\"name\":\"Widget v2\",\"description\":\"updated\",\"version\":99}";

        mockMvc.perform(
                        put("/api/v1/items/{id}", id)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(body))
                .andExpect(status().isConflict())
                .andExpect(jsonPath("$.code", is("ITEM_VERSION_CONFLICT")));
    }

    @Test
    void should_clear_description_when_patched_with_explicit_null() throws Exception {
        String id = createItem("Widget", "desc").get("id").asText();

        mockMvc.perform(
                        patch("/api/v1/items/{id}", id)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"description\":null}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.description", is(nullValue())));
    }

    @Test
    void should_leave_description_unchanged_when_patch_omits_it() throws Exception {
        String id = createItem("Widget", "keep").get("id").asText();

        mockMvc.perform(
                        patch("/api/v1/items/{id}", id)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content("{\"name\":\"Renamed\"}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is("Renamed")))
                .andExpect(jsonPath("$.description", is("keep")));
    }

    @Test
    void should_delete_item_and_be_idempotent() throws Exception {
        String id = createItem("Widget", "desc").get("id").asText();

        mockMvc.perform(delete("/api/v1/items/{id}", id)).andExpect(status().isNoContent());
        // Second delete of the now-missing item is still 204.
        mockMvc.perform(delete("/api/v1/items/{id}", id)).andExpect(status().isNoContent());
    }
}
