package com.zioinfo.kintex.tenant;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zioinfo.kintex.auth.AuthProperties;
import com.zioinfo.kintex.auth.EventRole;
import com.zioinfo.kintex.auth.JwtService;
import com.zioinfo.kintex.common.error.ErrorCode;
import jakarta.servlet.FilterChain;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * 멀티테넌시 2단계 격리 증명 — {@link TenantContextFilter}의 JWT tid 권위·크로스-테넌트 가드 단위테스트(§1A-3-2).
 * <p>필터가 세팅한 {@code TenantContext}가 곧 전 매퍼의 스코프 파라미터이므로, 이 컨텍스트가 요청별로
 * 올바르게 격리·거부되는지가 스코핑 격리의 근간이다(DB 없이 메커니즘 자체를 증명).
 */
class TenantContextFilterTest {

    private static final String SECRET = "kintex-test-secret-key-0123456789-abcdef"; // >= 32 bytes

    private JwtService jwt;
    private TenantResolver resolver;
    private TenantContextFilter filter;

    @BeforeEach
    void setUp() {
        AuthProperties props = new AuthProperties();
        props.setJwtSecret(SECRET);
        props.setAccessTokenTtlSeconds(3600);
        jwt = new JwtService(props);
        resolver = mock(TenantResolver.class);
        when(resolver.exists("DEMO")).thenReturn(true);
        when(resolver.exists("KINTEX")).thenReturn(true);
        filter = new TenantContextFilter(resolver, new ObjectMapper(), jwt);
    }

    /** 인증 요청은 JWT tid 가 권위 — tid=DEMO 토큰이면 컨텍스트도 DEMO(헤더 없음). */
    @Test
    void authenticatedRequestUsesJwtTidAsAuthority() throws Exception {
        String token = jwt.issue("u1", "사용자", Map.of("e1", EventRole.EXHIBITOR), false, "EXHIBITOR", "DEMO");
        Result r = run(token, null);
        assertEquals("DEMO", r.tenant);
        assertEquals(200, r.status);
    }

    /** tid 부재 레거시·기본 토큰 → 기준 테넌트 KINTEX(회귀 0). */
    @Test
    void defaultTenantForTokenWithoutExplicitTid() throws Exception {
        String token = jwt.issue("u1", "사용자", Map.of(), false); // tid 미지정 → KINTEX
        Result r = run(token, null);
        assertEquals("KINTEX", r.tenant);
        assertEquals(200, r.status);
    }

    /** 플랫폼 관리자(ADMIN)는 X-Tenant-Id 로 대상 테넌트 스위치 허용. */
    @Test
    void adminMaySwitchTenantViaHeader() throws Exception {
        String token = jwt.issue("admin", "관리자", Map.of(), false, "ADMIN", "KINTEX");
        Result r = run(token, "DEMO");
        assertEquals("DEMO", r.tenant);
        assertEquals(200, r.status);
    }

    /** 홀매니저(플랫폼 운영축)도 스위치 허용. */
    @Test
    void hallManagerMaySwitchTenantViaHeader() throws Exception {
        String token = jwt.issue("hm", "홀매니저", Map.of(), true, "MANAGER", "KINTEX");
        Result r = run(token, "DEMO");
        assertEquals("DEMO", r.tenant);
        assertEquals(200, r.status);
    }

    /** 비관리자가 자기 tid 와 다른 테넌트를 헤더로 시도하면 403(하이재킹 차단) — 체인 미실행. */
    @Test
    void nonAdminCrossTenantHeaderIsForbidden() throws Exception {
        String token = jwt.issue("u1", "참가자", Map.of("e1", EventRole.EXHIBITOR), false, "EXHIBITOR", "KINTEX");
        Result r = run(token, "DEMO");
        assertNull(r.tenant, "체인이 실행되지 않아야 한다(거부)");
        assertEquals(ErrorCode.FORBIDDEN.status().value(), r.status);
    }

    /** 동일 테넌트를 헤더로 명시하는 것은 비관리자라도 허용(스위치 아님). */
    @Test
    void sameTenantHeaderAllowedForNonAdmin() throws Exception {
        String token = jwt.issue("u1", "참가자", Map.of(), false, "EXHIBITOR", "KINTEX");
        Result r = run(token, "KINTEX");
        assertEquals("KINTEX", r.tenant);
        assertEquals(200, r.status);
    }

    /** 관리자가 미존재 테넌트로 스위치 시도하면 400. */
    @Test
    void adminSwitchToUnknownTenantIsBadRequest() throws Exception {
        String token = jwt.issue("admin", "관리자", Map.of(), false, "ADMIN", "KINTEX");
        Result r = run(token, "GHOST");
        assertNull(r.tenant);
        assertEquals(ErrorCode.VALIDATION.status().value(), r.status);
    }

    /** 미인증(공개 경로) + 헤더/도메인 미매칭 → 기준 테넌트 KINTEX. */
    @Test
    void anonymousFallsBackToDefaultTenant() throws Exception {
        Result r = run(null, null);
        assertEquals("KINTEX", r.tenant);
        assertEquals(200, r.status);
    }

    // ── 헬퍼 ─────────────────────────────────────────────────────────────
    private record Result(String tenant, int status) {
    }

    private Result run(String bearer, String tenantHeader) throws Exception {
        MockHttpServletRequest req = new MockHttpServletRequest();
        if (bearer != null) {
            req.addHeader("Authorization", "Bearer " + bearer);
        }
        if (tenantHeader != null) {
            req.addHeader("X-Tenant-Id", tenantHeader);
        }
        MockHttpServletResponse res = new MockHttpServletResponse();
        AtomicReference<String> seen = new AtomicReference<>();
        FilterChain chain = (rq, rs) -> seen.set(TenantContext.currentTenantId());
        filter.doFilter(req, res, chain);
        // 체인 실행 후 컨텍스트는 finally 에서 clear 되어야 한다(스레드 누수 방지).
        assertNull(TenantContext.rawTenantId(), "요청 종료 후 TenantContext 는 정리되어야 한다");
        return new Result(seen.get(), res.getStatus());
    }
}
