package com.lingniu.ingest.session;

import com.lingniu.ingest.api.ProtocolId;
import org.junit.jupiter.api.Test;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.time.Duration;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class RedisSessionStoreTest {

    @Test
    void putStoresSessionHashAndThreeIndexesWithTtl() {
        RedisFixture fixture = new RedisFixture();
        RedisSessionStore store = new RedisSessionStore(fixture.redis, Duration.ofMinutes(45));
        DeviceSession session = sampleSession("sid-1", "VIN001", "13800138000");

        store.put(session);

        verify(fixture.hashOps).putAll(eq("vehicle:session:sid-1"), any(Map.class));
        verify(fixture.valueOps).set("vehicle:session:index:vin:VIN001", "sid-1", Duration.ofMinutes(45));
        verify(fixture.valueOps).set("vehicle:session:index:phone:13800138000", "sid-1", Duration.ofMinutes(45));
        verify(fixture.setOps).add("vehicle:session:index:ids", "sid-1");
        verify(fixture.redis).expire("vehicle:session:sid-1", Duration.ofMinutes(45));
        verify(fixture.redis).expire("vehicle:session:index:ids", Duration.ofMinutes(45));
    }

    @Test
    void findsBySessionIdVinAndPhoneFromRedisIndexes() {
        RedisFixture fixture = new RedisFixture();
        RedisSessionStore store = new RedisSessionStore(fixture.redis, Duration.ofMinutes(30));
        Map<Object, Object> stored = storedSession("sid-1", "VIN001", "13800138000");
        when(fixture.hashOps.entries("vehicle:session:sid-1")).thenReturn(stored);
        when(fixture.valueOps.get("vehicle:session:index:vin:VIN001")).thenReturn("sid-1");
        when(fixture.valueOps.get("vehicle:session:index:phone:13800138000")).thenReturn("sid-1");

        Optional<DeviceSession> byId = store.findBySessionId("sid-1");
        Optional<DeviceSession> byVin = store.findByVin("VIN001");
        Optional<DeviceSession> byPhone = store.findByPhone("13800138000");

        assertThat(byId).isPresent();
        assertThat(byVin).contains(byId.orElseThrow());
        assertThat(byPhone).contains(byId.orElseThrow());
        assertThat(byId.orElseThrow().attributes()).containsEntry("identitySource", "BINDING");
        verify(fixture.redis, atLeastOnce()).expire("vehicle:session:sid-1", Duration.ofMinutes(30));
    }

    @Test
    void updateRewritesIndexesWhenVinChangesAndRemoveDeletesAllKeys() {
        RedisFixture fixture = new RedisFixture();
        RedisSessionStore store = new RedisSessionStore(fixture.redis, Duration.ofMinutes(30));
        when(fixture.hashOps.entries("vehicle:session:sid-1"))
                .thenReturn(storedSession("sid-1", "VIN001", "13800138000"))
                .thenReturn(storedSession("sid-1", "VIN001", "13800138000"))
                .thenReturn(storedSession("sid-1", "VIN002", "13800138000"));

        Optional<DeviceSession> updated = store.update("sid-1", session -> session.withVin("VIN002"));
        store.remove("sid-1");

        assertThat(updated).isPresent();
        verify(fixture.redis).delete("vehicle:session:index:vin:VIN001");
        verify(fixture.valueOps).set("vehicle:session:index:vin:VIN002", "sid-1", Duration.ofMinutes(30));
        verify(fixture.redis).delete("vehicle:session:sid-1");
        verify(fixture.redis).delete("vehicle:session:index:vin:VIN002");
        verify(fixture.redis, atLeastOnce()).delete("vehicle:session:index:phone:13800138000");
        verify(fixture.setOps).remove("vehicle:session:index:ids", "sid-1");
    }

    @Test
    void sizeReturnsRedisIdSetCardinality() {
        RedisFixture fixture = new RedisFixture();
        RedisSessionStore store = new RedisSessionStore(fixture.redis, Duration.ofMinutes(30));
        when(fixture.setOps.size("vehicle:session:index:ids")).thenReturn(12L);

        assertThat(store.size()).isEqualTo(12);
    }

    private static DeviceSession sampleSession(String sid, String vin, String phone) {
        return new DeviceSession(
                sid,
                ProtocolId.JT808,
                vin,
                phone,
                "token-1",
                "127.0.0.1:9000",
                Instant.parse("2026-06-22T08:00:00Z"),
                Instant.parse("2026-06-22T08:01:00Z"),
                Map.of("identitySource", "BINDING"));
    }

    private static Map<Object, Object> storedSession(String sid, String vin, String phone) {
        Map<Object, Object> values = new LinkedHashMap<>();
        values.put("sessionId", sid);
        values.put("protocol", "JT808");
        values.put("vin", vin);
        values.put("phone", phone);
        values.put("token", "token-1");
        values.put("peerAddress", "127.0.0.1:9000");
        values.put("registeredAt", "2026-06-22T08:00:00Z");
        values.put("lastSeenAt", "2026-06-22T08:01:00Z");
        values.put("attr.identitySource", "BINDING");
        return values;
    }

    private static final class RedisFixture {
        private final StringRedisTemplate redis = mock(StringRedisTemplate.class);
        @SuppressWarnings("unchecked")
        private final HashOperations<String, Object, Object> hashOps = mock(HashOperations.class);
        @SuppressWarnings("unchecked")
        private final ValueOperations<String, String> valueOps = mock(ValueOperations.class);
        @SuppressWarnings("unchecked")
        private final SetOperations<String, String> setOps = mock(SetOperations.class);

        private RedisFixture() {
            when(redis.opsForHash()).thenReturn(hashOps);
            when(redis.opsForValue()).thenReturn(valueOps);
            when(redis.opsForSet()).thenReturn(setOps);
            when(hashOps.entries("vehicle:session:sid-1")).thenReturn(Map.of());
            when(setOps.members("vehicle:session:index:ids")).thenReturn(Set.of());
        }
    }
}
