package incheon.product.common.geo;

import incheon.com.cmm.exception.BusinessException;
import incheon.product.common.config.GeoViewProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.*;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * GisServerClient 단위 테스트.
 * RestTemplate mock으로 GIS 서버 REST API 호출 로직을 검증한다.
 */
@ExtendWith(MockitoExtension.class)
class GisServerClientTest {

    private GisServerClient client;

    @Mock
    private RestTemplate restTemplate;

    private GeoViewProperties properties;

    @BeforeEach
    void setup() {
        client = new GisServerClient();
        properties = new GeoViewProperties();
        properties.getGisServer().setUrl("http://gis.test.com");
        properties.getGisServer().setSyncWaitMillis(0L);
        ReflectionTestUtils.setField(client, "properties", properties);
        ReflectionTestUtils.setField(client, "restTemplate", restTemplate);
    }

    @Test
    void publishLayerSuccess() {
        when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(Map.class)))
                .thenReturn(ResponseEntity.ok(Map.of()));

        boolean result = client.publishLayer("ws", "lyr", "store", "tbl", 5181,
                1.0, 2.0, 3.0, 4.0);

        assertThat(result).isTrue();
        verify(restTemplate).postForEntity(
                eq("http://gis.test.com/rest/layer"), any(HttpEntity.class), eq(Map.class));
    }

    @Test
    void publishLayerRetryOnT001WithFallbackBbox() {
        HttpClientErrorException notFound = HttpClientErrorException.create(
                HttpStatus.NOT_FOUND, "Not Found", new HttpHeaders(),
                "{\"code\":\"T001\"}".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);

        when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(Map.class)))
                .thenThrow(notFound)
                .thenReturn(ResponseEntity.ok(Map.of()));

        boolean result = client.publishLayer("ws", "lyr", "store", "tbl", 5181,
                null, null, null, null);

        assertThat(result).isTrue();
        verify(restTemplate, times(2)).postForEntity(anyString(), any(HttpEntity.class), eq(Map.class));
    }

    @Test
    void publishLayerNonRetryableThrows() {
        HttpClientErrorException badRequest = HttpClientErrorException.create(
                HttpStatus.BAD_REQUEST, "Bad Request", new HttpHeaders(),
                "{\"code\":\"UNKNOWN\"}".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);

        when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(Map.class)))
                .thenThrow(badRequest);

        assertThatThrownBy(() -> client.publishLayer("ws", "lyr", "store", "tbl", 5181,
                null, null, null, null))
                .isInstanceOf(HttpClientErrorException.class);
    }

    @SuppressWarnings("unchecked")
    @Test
    void publishLayerShorthandUsesDefaultStorage() {
        when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(Map.class)))
                .thenReturn(ResponseEntity.ok(Map.of()));

        client.publishLayer("ws", "lyr", "schema.tbl", 5181);

        ArgumentCaptor<HttpEntity<Map<String, Object>>> captor = ArgumentCaptor.forClass(HttpEntity.class);
        verify(restTemplate).postForEntity(anyString(), captor.capture(), eq(Map.class));
        Map<String, Object> body = (Map<String, Object>) captor.getValue().getBody();
        assertThat(body).containsEntry("storage", "iccom");
    }

    @SuppressWarnings("unchecked")
    @Test
    void updateLayerConditionalBodyBuilding() {
        when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Map.class)))
                .thenReturn(ResponseEntity.ok(Map.of()));

        boolean result = client.updateLayer("ws", "lyr", 5181, 1.0, 2.0, 3.0, 4.0);

        assertThat(result).isTrue();
        ArgumentCaptor<HttpEntity<Map<String, Object>>> captor = ArgumentCaptor.forClass(HttpEntity.class);
        verify(restTemplate).exchange(
                eq("http://gis.test.com/rest/workspace/ws/layer/lyr"),
                eq(HttpMethod.PUT), captor.capture(), eq(Map.class));
        Map<String, Object> body = (Map<String, Object>) captor.getValue().getBody();
        assertThat(body)
                .containsEntry("serviceSrid", 5181)
                .containsEntry("minx", 1.0)
                .containsEntry("maxy", 4.0);
    }

    @Test
    void updateLayerNotFoundThrows() {
        HttpClientErrorException notFound = HttpClientErrorException.create(
                HttpStatus.NOT_FOUND, "Not Found", new HttpHeaders(), new byte[0], null);

        when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(Map.class)))
                .thenThrow(notFound);

        assertThatThrownBy(() -> client.updateLayer("ws", "lyr", 5181, null, null, null, null))
                .isInstanceOf(RestClientException.class)
                .hasMessageContaining("ws/lyr");
    }

    @Test
    void deleteLayerSuccess() {
        when(restTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Map.class)))
                .thenReturn(ResponseEntity.ok(Map.of()));

        assertThat(client.deleteLayer("ws", "lyr")).isTrue();
    }

    @Test
    void deleteLayerNotFoundReturnsFalse() {
        HttpClientErrorException notFound = HttpClientErrorException.create(
                HttpStatus.NOT_FOUND, "Not Found", new HttpHeaders(), new byte[0], null);

        when(restTemplate.exchange(anyString(), eq(HttpMethod.DELETE), any(HttpEntity.class), eq(Map.class)))
                .thenThrow(notFound);

        assertThat(client.deleteLayer("ws", "lyr")).isFalse();
    }

    @Test
    void getLayerInfoReturnsBody() {
        Map<String, Object> info = Map.of("name", "layer1", "workspace", "ws");
        when(restTemplate.getForEntity(anyString(), eq(Map.class)))
                .thenReturn(ResponseEntity.ok(info));

        Map<String, Object> result = client.getLayerInfo("ws", "layer1");

        assertThat(result).containsEntry("name", "layer1");
    }

    @Test
    void getLayerInfoNotFoundReturnsNull() {
        HttpClientErrorException notFound = HttpClientErrorException.create(
                HttpStatus.NOT_FOUND, "Not Found", new HttpHeaders(), new byte[0], null);

        when(restTemplate.getForEntity(anyString(), eq(Map.class)))
                .thenThrow(notFound);

        assertThat(client.getLayerInfo("ws", "lyr")).isNull();
    }

    @Test
    void addStyleSuccess() {
        when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(Map.class)))
                .thenReturn(ResponseEntity.ok(Map.of()));

        assertThat(client.addStyle("style1", "<sld>xml</sld>")).isTrue();
        verify(restTemplate).postForEntity(
                eq("http://gis.test.com/rest/style"), any(HttpEntity.class), eq(Map.class));
    }

    @Test
    void getSldReturnsBody() {
        when(restTemplate.getForEntity(anyString(), eq(String.class)))
                .thenReturn(ResponseEntity.ok("<sld>xml</sld>"));

        assertThat(client.getSld("style1")).isEqualTo("<sld>xml</sld>");
        verify(restTemplate).getForEntity("http://gis.test.com/rest/sld/style1", String.class);
    }

    @Test
    void baseUrlBlankThrowsBusinessException() {
        properties.getGisServer().setUrl("");

        assertThatThrownBy(() -> client.publishLayer("ws", "lyr", "store", "tbl", 5181,
                null, null, null, null))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("url 설정");
    }
}
