package com.zioinfo.kintex.cms;

import com.zioinfo.kintex.ai.AiTextRouter;
import com.zioinfo.kintex.ai.AiTextRouter.AiResult;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.cms.dto.CmsContentUpdateRequest;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.system.SystemAccessGuard;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

/**
 * M17 CMS 서비스 단위테스트 — 게시 전이 가드(전진만·권한)·본문 sanitize·버전 스냅샷·롤백·AI 번역(F100 배선).
 */
class CmsServiceTest {

    private CmsMapper mapper;
    private SystemAccessGuard scope;
    private AiTextRouter aiRouter;
    private CmsService service;

    private final KintexPrincipal editor =
            new KintexPrincipal("u1", "편집자", Map.of(), false);

    @BeforeEach
    void setUp() {
        mapper = mock(CmsMapper.class);
        scope = mock(SystemAccessGuard.class);
        aiRouter = mock(AiTextRouter.class);
        service = new CmsService(mapper, scope, aiRouter,
                mock(com.zioinfo.kintex.common.audit.AuditLogService.class), "./build/test-uploads");
    }

    private Map<String, Object> contentRow(String status) {
        Map<String, Object> r = new HashMap<>();
        r.put("id", "cms-1");
        r.put("title", "제목");
        r.put("body", "본문");
        r.put("status", status);
        r.put("contentType", "PAGE");
        return r;
    }

    // ── 상태 전이 가드 ─────────────────────────────────────────────────────────

    @Test
    void transitionRejectsBackwardMove() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("published"));
        ApiException ex = assertThrows(ApiException.class,
                () -> service.transition(editor, "cms-1", "review"));
        assertEquals(ErrorCode.VALIDATION, ex.getCode());
        verify(mapper, never()).updateStatus(anyString(), anyString());
    }

    @Test
    void transitionRejectsUnknownStatus() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("draft"));
        ApiException ex = assertThrows(ApiException.class,
                () -> service.transition(editor, "cms-1", "archived"));
        assertEquals(ErrorCode.VALIDATION, ex.getCode());
    }

    @Test
    void publishTransitionRequiresManagerAndSnapshots() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("approved"));
        when(mapper.maxVersionNo("cms-1")).thenReturn(2);
        service.transition(editor, "cms-1", "published");
        verify(scope).requireManager(editor);                 // 게시는 매니저↑ 게이트
        verify(mapper).updateStatus("cms-1", "published");
        verify(mapper).insertVersion(any());                  // 게시 시 버전 스냅샷
    }

    @Test
    void reviewRequestNeedsNoManagerGate() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("draft"));
        service.transition(editor, "cms-1", "review");
        verify(scope, never()).requireManager(any());
        verify(mapper).updateStatus("cms-1", "review");
    }

    // ── 본문 sanitize + 버전 스냅샷 ────────────────────────────────────────────

    @Test
    void updateSanitizesBodyBeforePersistAndSnapshots() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("draft"));
        when(mapper.maxVersionNo("cms-1")).thenReturn(0);
        var req = new CmsContentUpdateRequest(
                "제목", "<p>안전</p><script>alert('xss')</script>", null, true, false);

        service.update(editor, "cms-1", req);

        ArgumentCaptor<Map<String, Object>> cap = ArgumentCaptor.forClass(Map.class);
        verify(mapper).updateContent(cap.capture());
        String storedBody = String.valueOf(cap.getValue().get("body"));
        assertTrue(storedBody.contains("<p>안전</p>"), "허용 태그는 보존");
        assertFalse(storedBody.toLowerCase().contains("<script"), "script 태그 제거");
        assertFalse(storedBody.contains("alert"), "script 내용 제거");
        verify(mapper).insertVersion(any());                  // 저장 시 버전 스냅샷
    }

    @Test
    void updateRejectsBlankTitle() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("draft"));
        var req = new CmsContentUpdateRequest("   ", "본문", null, null, null);
        ApiException ex = assertThrows(ApiException.class, () -> service.update(editor, "cms-1", req));
        assertEquals(ErrorCode.VALIDATION, ex.getCode());
    }

    // ── 롤백 ───────────────────────────────────────────────────────────────────

    @Test
    void rollbackRestoresVersionAndSnapshots() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("published"));
        Map<String, Object> ver = new HashMap<>();
        ver.put("title", "예전제목");
        ver.put("body", "예전본문");
        ver.put("status", "review");
        when(mapper.findVersion("cms-1", 2)).thenReturn(ver);
        when(mapper.maxVersionNo("cms-1")).thenReturn(4);

        service.rollback(editor, "cms-1", 2);

        verify(scope).requireManager(editor);
        ArgumentCaptor<Map<String, Object>> cap = ArgumentCaptor.forClass(Map.class);
        verify(mapper).restoreContent(cap.capture());
        assertEquals("예전제목", cap.getValue().get("title"));
        assertEquals("review", cap.getValue().get("status"));
        verify(mapper).insertVersion(any());                  // 롤백 스냅샷
    }

    @Test
    void rollbackMissingVersionThrowsNotFound() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("draft"));
        when(mapper.findVersion("cms-1", 9)).thenReturn(null);
        ApiException ex = assertThrows(ApiException.class, () -> service.rollback(editor, "cms-1", 9));
        assertEquals(ErrorCode.NOT_FOUND, ex.getCode());
    }

    // ── AI 번역 (F100 배선) ──────────────────────────────────────────────────────

    /** AI 미가용(degraded) 시 환각 없이 degraded=true 결과(500 아님) — 저장하지 않는다. */
    @Test
    void aiTranslateDegradedReturnsDegradedResultAndDoesNotPersist() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("draft"));
        when(aiRouter.generate(anyString(), anyInt())).thenReturn(new AiResult(null, "none", true));
        var res = service.aiTranslate(editor, "cms-1", "en");
        assertTrue(res.degraded(), "AI 미가용은 degraded=true");
        assertNull(res.translation(), "degraded 시 번역 없음");
        verify(scope).requireManager(editor);                 // 매니저↑ 게이트
        verify(mapper, never()).upsertTranslation(any());      // 실패 시 저장 안 함
    }

    /** AI 응답이 파싱 불가하면 환각 없이 degraded=true — 저장하지 않는다. */
    @Test
    void aiTranslateUnparsableResponseDegradesAndDoesNotPersist() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("draft"));
        when(aiRouter.generate(anyString(), anyInt()))
                .thenReturn(new AiResult("죄송하지만 번역할 수 없습니다", "claude", false));
        var res = service.aiTranslate(editor, "cms-1", "en");
        assertTrue(res.degraded());
        verify(mapper, never()).upsertTranslation(any());
    }

    /** 대상 언어 화이트리스트 밖(ko 원문 언어 포함)은 거부(400). */
    @Test
    void aiTranslateRejectsSourceLang() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("draft"));
        ApiException ex = assertThrows(ApiException.class,
                () -> service.aiTranslate(editor, "cms-1", "ko"));
        assertEquals(ErrorCode.VALIDATION, ex.getCode());
    }

    /** 입력 길이 상한 초과 시 AI 호출 없이 400 — 과금 방어. */
    @Test
    void aiTranslateRejectsOversizedInput() {
        Map<String, Object> big = contentRow("draft");
        big.put("body", "가".repeat(12_001));
        when(mapper.findContentById("cms-1")).thenReturn(big);
        ApiException ex = assertThrows(ApiException.class,
                () -> service.aiTranslate(editor, "cms-1", "en"));
        assertEquals(ErrorCode.VALIDATION, ex.getCode());
        verify(aiRouter, never()).generate(anyString(), anyInt());
    }

    /** AI 성공 시 sanitize 후 trans_status='ai' 로 upsert + degraded=false 결과. */
    @Test
    void aiTranslatePersistsAiDraftOnSuccess() {
        when(mapper.findContentById("cms-1")).thenReturn(contentRow("draft"));
        when(aiRouter.generate(anyString(), anyInt()))
                .thenReturn(new AiResult("{\"title\":\"Title\",\"body\":\"<p>Body</p><script>x()</script>\"}", "claude", false));
        Map<String, Object> trRow = new HashMap<>();
        trRow.put("contentId", "cms-1");
        trRow.put("lang", "en");
        trRow.put("title", "Title");
        trRow.put("body", "<p>Body</p>");
        trRow.put("transStatus", "ai");
        when(mapper.findTranslations("cms-1")).thenReturn(java.util.List.of(trRow));
        var res = service.aiTranslate(editor, "cms-1", "en");
        assertFalse(res.degraded(), "성공은 degraded=false");
        assertNotNull(res.translation());
        assertEquals("claude", res.provider());
        ArgumentCaptor<Map<String, Object>> cap = ArgumentCaptor.forClass(Map.class);
        verify(mapper).upsertTranslation(cap.capture());
        assertEquals("en", cap.getValue().get("lang"));
        assertEquals("ai", cap.getValue().get("transStatus"));
        String body = String.valueOf(cap.getValue().get("body"));
        assertTrue(body.contains("<p>Body</p>"), "허용 태그 보존");
        assertFalse(body.toLowerCase().contains("<script"), "script 제거");
    }
}
