package gc.mda.signal_batch.batch.reader;

import gc.mda.signal_batch.domain.vessel.model.AisTargetEntity;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;

import static org.assertj.core.api.Assertions.assertThat;

class AisTargetCacheManagerTest {

    private AisTargetCacheManager cacheManager;

    @BeforeEach
    void setUp() {
        cacheManager = new AisTargetCacheManager();
        ReflectionTestUtils.setField(cacheManager, "ttlMinutes", 120L);
        ReflectionTestUtils.setField(cacheManager, "maxSize", 1000);
        cacheManager.init();
    }

    private AisTargetEntity createEntity(String mmsi, OffsetDateTime timestamp) {
        return AisTargetEntity.builder()
                .mmsi(mmsi)
                .lat(34.0)
                .lon(127.0)
                .sog(10.0)
                .messageTimestamp(timestamp)
                .build();
    }

    @Nested
    @DisplayName("단건 조회/저장")
    class SingleOperations {

        @Test
        @DisplayName("get — 존재하는 MMSI")
        void get_existing_returnsEntity() {
            OffsetDateTime ts = OffsetDateTime.now(ZoneOffset.UTC);
            cacheManager.put(createEntity("440331240", ts));

            Optional<AisTargetEntity> result = cacheManager.get("440331240");

            assertThat(result).isPresent();
            assertThat(result.get().getMmsi()).isEqualTo("440331240");
        }

        @Test
        @DisplayName("get — 없는 MMSI")
        void get_nonExisting_returnsEmpty() {
            assertThat(cacheManager.get("999999999")).isEmpty();
        }

        @Test
        @DisplayName("put — 신규 저장")
        void put_newEntity_stored() {
            OffsetDateTime ts = OffsetDateTime.now(ZoneOffset.UTC);
            cacheManager.put(createEntity("440331240", ts));

            assertThat(cacheManager.size()).isEqualTo(1);
        }

        @Test
        @DisplayName("put — null entity 무시")
        void put_null_ignored() {
            cacheManager.put(null);

            assertThat(cacheManager.size()).isEqualTo(0);
        }

        @Test
        @DisplayName("put — mmsi null entity 무시")
        void put_nullMmsi_ignored() {
            cacheManager.put(AisTargetEntity.builder().lat(34.0).lon(127.0).build());

            assertThat(cacheManager.size()).isEqualTo(0);
        }
    }

    @Nested
    @DisplayName("timestamp 비교 (isNewer)")
    class TimestampComparison {

        @Test
        @DisplayName("최신 데이터로 업데이트")
        void put_newerTimestamp_updates() {
            OffsetDateTime older = OffsetDateTime.of(2025, 1, 1, 10, 0, 0, 0, ZoneOffset.UTC);
            OffsetDateTime newer = OffsetDateTime.of(2025, 1, 1, 11, 0, 0, 0, ZoneOffset.UTC);

            cacheManager.put(createEntity("440331240", older));
            cacheManager.put(createEntity("440331240", newer));

            AisTargetEntity entity = cacheManager.get("440331240").orElseThrow();
            assertThat(entity.getMessageTimestamp()).isEqualTo(newer);
        }

        @Test
        @DisplayName("이전 데이터는 업데이트하지 않음")
        void put_olderTimestamp_skipped() {
            OffsetDateTime newer = OffsetDateTime.of(2025, 1, 1, 11, 0, 0, 0, ZoneOffset.UTC);
            OffsetDateTime older = OffsetDateTime.of(2025, 1, 1, 10, 0, 0, 0, ZoneOffset.UTC);

            cacheManager.put(createEntity("440331240", newer));
            cacheManager.put(createEntity("440331240", older));

            AisTargetEntity entity = cacheManager.get("440331240").orElseThrow();
            assertThat(entity.getMessageTimestamp()).isEqualTo(newer);
        }

        @Test
        @DisplayName("기존 timestamp=null → 새 데이터로 업데이트")
        void put_existingTimestampNull_updates() {
            cacheManager.put(createEntity("440331240", null));

            OffsetDateTime ts = OffsetDateTime.now(ZoneOffset.UTC);
            cacheManager.put(createEntity("440331240", ts));

            AisTargetEntity entity = cacheManager.get("440331240").orElseThrow();
            assertThat(entity.getMessageTimestamp()).isEqualTo(ts);
        }

        @Test
        @DisplayName("새 데이터의 timestamp=null → 업데이트하지 않음")
        void put_newTimestampNull_skipped() {
            OffsetDateTime ts = OffsetDateTime.now(ZoneOffset.UTC);
            cacheManager.put(createEntity("440331240", ts));
            cacheManager.put(createEntity("440331240", null));

            AisTargetEntity entity = cacheManager.get("440331240").orElseThrow();
            assertThat(entity.getMessageTimestamp()).isEqualTo(ts);
        }
    }

    @Nested
    @DisplayName("배치 조회/업데이트")
    class BatchOperations {

        @Test
        @DisplayName("getAll — 존재하는 MMSI들 조회")
        void getAll_existing_returnsMap() {
            OffsetDateTime ts = OffsetDateTime.now(ZoneOffset.UTC);
            cacheManager.put(createEntity("111111111", ts));
            cacheManager.put(createEntity("222222222", ts));
            cacheManager.put(createEntity("333333333", ts));

            Map<String, AisTargetEntity> result = cacheManager.getAll(List.of("111111111", "333333333"));

            assertThat(result).hasSize(2);
            assertThat(result).containsKeys("111111111", "333333333");
        }

        @Test
        @DisplayName("getAll — null 리스트")
        void getAll_null_returnsEmpty() {
            assertThat(cacheManager.getAll(null)).isEmpty();
        }

        @Test
        @DisplayName("getAll — 빈 리스트")
        void getAll_empty_returnsEmpty() {
            assertThat(cacheManager.getAll(List.of())).isEmpty();
        }

        @Test
        @DisplayName("putAll — 정상 배치 저장")
        void putAll_multipleEntities_stored() {
            OffsetDateTime ts = OffsetDateTime.now(ZoneOffset.UTC);
            List<AisTargetEntity> entities = List.of(
                    createEntity("111111111", ts),
                    createEntity("222222222", ts),
                    createEntity("333333333", ts)
            );

            cacheManager.putAll(entities);

            assertThat(cacheManager.size()).isEqualTo(3);
        }

        @Test
        @DisplayName("putAll — null 리스트 무시")
        void putAll_null_ignored() {
            cacheManager.putAll(null);

            assertThat(cacheManager.size()).isEqualTo(0);
        }

        @Test
        @DisplayName("putAll — 빈 리스트 무시")
        void putAll_empty_ignored() {
            cacheManager.putAll(List.of());

            assertThat(cacheManager.size()).isEqualTo(0);
        }

        @Test
        @DisplayName("putAll — 신규/업데이트/스킵 혼합")
        void putAll_mixedUpdates() {
            OffsetDateTime t1 = OffsetDateTime.of(2025, 1, 1, 10, 0, 0, 0, ZoneOffset.UTC);
            OffsetDateTime t2 = OffsetDateTime.of(2025, 1, 1, 11, 0, 0, 0, ZoneOffset.UTC);
            OffsetDateTime t0 = OffsetDateTime.of(2025, 1, 1, 9, 0, 0, 0, ZoneOffset.UTC);

            // 기존 데이터
            cacheManager.put(createEntity("111111111", t1));

            // 배치: 111=이전(스킵), 222=신규, 333=신규
            List<AisTargetEntity> batch = List.of(
                    createEntity("111111111", t0),  // 이전 → 스킵
                    createEntity("222222222", t2),   // 신규
                    createEntity("333333333", t1)    // 신규
            );
            cacheManager.putAll(batch);

            assertThat(cacheManager.size()).isEqualTo(3);
            // 111은 기존 t1 유지
            assertThat(cacheManager.get("111111111").get().getMessageTimestamp()).isEqualTo(t1);
        }
    }

    @Nested
    @DisplayName("캐시 스냅샷 및 필터링")
    class SnapshotAndFilter {

        @Test
        @DisplayName("getAllValues — 전체 값 조회")
        void getAllValues_returnsAll() {
            OffsetDateTime ts = OffsetDateTime.now(ZoneOffset.UTC);
            cacheManager.put(createEntity("111111111", ts));
            cacheManager.put(createEntity("222222222", ts));

            assertThat(cacheManager.getAllValues()).hasSize(2);
        }

        @Test
        @DisplayName("getByTimeRange — 시간 범위 내 데이터만 반환")
        void getByTimeRange_filtersCorrectly() {
            OffsetDateTime recent = OffsetDateTime.now(ZoneOffset.UTC).minusMinutes(5);
            OffsetDateTime old = OffsetDateTime.now(ZoneOffset.UTC).minusMinutes(120);

            cacheManager.put(createEntity("111111111", recent));
            cacheManager.put(createEntity("222222222", old));

            List<AisTargetEntity> result = cacheManager.getByTimeRange(60);

            assertThat(result).hasSize(1);
            assertThat(result.get(0).getMmsi()).isEqualTo("111111111");
        }

        @Test
        @DisplayName("getByTimeRange — timestamp=null 데이터 제외")
        void getByTimeRange_excludesNullTimestamp() {
            OffsetDateTime recent = OffsetDateTime.now(ZoneOffset.UTC).minusMinutes(5);

            cacheManager.put(createEntity("111111111", recent));
            cacheManager.put(createEntity("222222222", null));

            List<AisTargetEntity> result = cacheManager.getByTimeRange(60);

            assertThat(result).hasSize(1);
        }
    }

    @Nested
    @DisplayName("캐시 관리")
    class CacheManagement {

        @Test
        @DisplayName("evict — 개별 삭제")
        void evict_removes() {
            cacheManager.put(createEntity("440331240", OffsetDateTime.now(ZoneOffset.UTC)));

            cacheManager.evict("440331240");

            assertThat(cacheManager.get("440331240")).isEmpty();
        }

        @Test
        @DisplayName("clear — 전체 삭제")
        void clear_removesAll() {
            OffsetDateTime ts = OffsetDateTime.now(ZoneOffset.UTC);
            cacheManager.put(createEntity("111111111", ts));
            cacheManager.put(createEntity("222222222", ts));

            cacheManager.clear();

            assertThat(cacheManager.size()).isEqualTo(0);
        }

        @Test
        @DisplayName("getStats — 통계 반환")
        void getStats_returnsMap() {
            cacheManager.put(createEntity("440331240", OffsetDateTime.now(ZoneOffset.UTC)));
            cacheManager.get("440331240"); // hit
            cacheManager.get("999999999"); // miss

            Map<String, Object> stats = cacheManager.getStats();

            assertThat(stats).containsKey("estimatedSize");
            assertThat(stats).containsKey("hitCount");
            assertThat(stats).containsKey("missCount");
            assertThat(stats).containsKey("maxSize");
        }
    }
}
