package com.zioinfo.kintex.boothsales;

import com.zioinfo.kintex.auth.EventRole;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.boothsales.dto.BoothSalesDtos.*;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.settlement.SettlementService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * F005/F006 부스 판매 서비스 단위테스트 — 참가업체 인벤토리 필터(available+본인 hold)·서버 권위 가격
 * (override vs 면적×요율)·이중 hold/만료 구매 409·매니저 전용(block/price/settle) 403 검증.
 */
class BoothSalesServiceTest {

    private static final String EVENT = "e-2026-smf";
    private static final long RATE = 145_000L; // 원/㎡

    private BoothSalesMapper mapper;
    private SettlementService settlement;
    private BoothSalesService service;

    private KintexPrincipal exhibitor;
    private KintexPrincipal manager;

    @BeforeEach
    void setUp() {
        mapper = mock(BoothSalesMapper.class);
        settlement = mock(SettlementService.class);
        service = new BoothSalesService(mapper, settlement);
        when(mapper.findRatePerM2()).thenReturn((double) RATE);

        exhibitor = new KintexPrincipal("u-ex", "참가업체A", Map.of(EVENT, EventRole.EXHIBITOR), false);
        manager = new KintexPrincipal("u-mgr", "홀매니저", Map.of(), true);
    }

    private Map<String, Object> row(String boothId, String status, Long unitPrice, String heldBy, double area) {
        Map<String, Object> m = new HashMap<>();
        m.put("boothId", boothId);
        m.put("boothNo", boothId);
        m.put("boothType", "independent");
        m.put("hallId", "H1");
        m.put("hallLabel", "제1전시장 홀1");
        m.put("areaM2", area);
        m.put("status", status);
        m.put("unitPrice", unitPrice);
        m.put("heldBy", heldBy);
        m.put("heldUntil", null);
        m.put("soldToCompany", null);
        return m;
    }

    @Test
    void exhibitorSeesOnlyAvailableAndOwnHold_withServerPrice() {
        List<Map<String, Object>> rows = List.of(
                row("b1", "available", null, null, 100.0),      // 보임 — 파생가 100×145000
                row("b2", "held", null, "u-ex", 50.0),          // 보임 — 본인 hold
                row("b3", "held", null, "u-other", 60.0),       // 숨김 — 타인 hold
                row("b4", "sold", null, "u-other", 70.0),       // 숨김 — 판매됨
                row("b5", "available", 9_000_000L, null, 40.0)  // 보임 — override 가격
        );
        when(mapper.findInventory(anyString(), eq(EVENT), any())).thenReturn(rows);

        InventoryResponse inv = service.inventory(EVENT, null, exhibitor);

        // 요약은 전 상태 반영(필터 전 카운트: available b1,b5 / held b2,b3 / sold b4), 목록은 참가업체 가시분만.
        assertEquals(2, inv.available());
        assertEquals(2, inv.held());
        assertEquals(1, inv.sold());
        assertFalse(inv.canManage());
        assertEquals(3, inv.units().size()); // available b1,b5 + 본인 hold b2

        BoothSaleUnit b1 = inv.units().stream().filter(u -> u.boothId().equals("b1")).findFirst().orElseThrow();
        assertEquals(100.0 * RATE, b1.unitPrice()); // 서버 권위 파생가
        assertFalse(b1.heldByMe());

        BoothSaleUnit b2 = inv.units().stream().filter(u -> u.boothId().equals("b2")).findFirst().orElseThrow();
        assertTrue(b2.heldByMe());

        BoothSaleUnit b5 = inv.units().stream().filter(u -> u.boothId().equals("b5")).findFirst().orElseThrow();
        assertEquals(9_000_000L, b5.unitPrice()); // override 우선
    }

    @Test
    void managerSeesAllStatuses() {
        List<Map<String, Object>> rows = List.of(
                row("b3", "held", null, "u-other", 60.0),
                row("b4", "sold", null, "u-other", 70.0),
                row("b6", "blocked", null, null, 30.0)
        );
        when(mapper.findInventory(anyString(), eq(EVENT), any())).thenReturn(rows);

        InventoryResponse inv = service.inventory(EVENT, null, manager);
        assertTrue(inv.canManage());
        assertEquals(3, inv.units().size()); // 전 상태 노출
    }

    @Test
    void holdConflictWhenAtomicTransitionAffectsZeroRows() {
        Map<String, Object> booth = new HashMap<>();
        booth.put("boothId", "b1");
        booth.put("boothNo", "b1");
        booth.put("hallId", "H1");
        booth.put("areaM2", 100.0);
        when(mapper.findBooth(EVENT, "b1")).thenReturn(booth);
        when(mapper.holdBooth(any())).thenReturn(0); // 이중 hold — 원자 전이 실패

        ApiException ex = assertThrows(ApiException.class,
                () -> service.hold(new HoldRequest(EVENT, "b1"), exhibitor));
        assertEquals(ErrorCode.CONFLICT, ex.getCode());
    }

    @Test
    void purchaseConflictWhenHoldExpiredOrNotHolder() {
        Map<String, Object> booth = new HashMap<>();
        booth.put("boothId", "b1");
        booth.put("boothNo", "b1");
        booth.put("hallId", "H1");
        booth.put("areaM2", 100.0);
        booth.put("assignedCompany", "참가업체A");
        when(mapper.findBooth(EVENT, "b1")).thenReturn(booth);
        when(mapper.purchaseBooth(any())).thenReturn(0);

        ApiException ex = assertThrows(ApiException.class,
                () -> service.purchase(new PurchaseRequest(EVENT, "b1", null), exhibitor));
        assertEquals(ErrorCode.CONFLICT, ex.getCode());
    }

    @Test
    void managementRequiresManager() {
        // 참가업체는 block/price/settle 불가 → FORBIDDEN (mapper 호출 이전 차단).
        ApiException block = assertThrows(ApiException.class,
                () -> service.setBlocked(new BlockRequest(EVENT, "b1", true, "통로 확보"), exhibitor));
        assertEquals(ErrorCode.FORBIDDEN, block.getCode());

        ApiException price = assertThrows(ApiException.class,
                () -> service.setPrice(new PriceRequest(EVENT, "b1", 5_000_000L), exhibitor));
        assertEquals(ErrorCode.FORBIDDEN, price.getCode());

        ApiException settle = assertThrows(ApiException.class,
                () -> service.settle(EVENT, exhibitor));
        assertEquals(ErrorCode.FORBIDDEN, settle.getCode());
    }
}
