package com.lingniu.ingest.protocol.jt1078.config;

import com.lingniu.ingest.core.concurrency.DisruptorEventBus;
import com.lingniu.ingest.core.config.IngestCoreAutoConfiguration;
import com.lingniu.ingest.api.event.VehicleEvent;
import com.lingniu.ingest.api.sink.EventSink;
import com.lingniu.ingest.identity.VehicleIdentity;
import com.lingniu.ingest.identity.VehicleIdentityRegistry;
import com.lingniu.ingest.identity.VehicleIdentityResolver;
import com.lingniu.ingest.identity.VehicleIdentitySource;
import com.lingniu.ingest.identity.config.VehicleIdentityAutoConfiguration;
import com.lingniu.ingest.protocol.jt1078.handler.Jt1078MediaSignalHandler;
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MalformedRtpEventPublisher;
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MalformedRtpPacket;
import com.lingniu.ingest.protocol.jt1078.media.Jt1078MediaArchiveService;
import com.lingniu.ingest.protocol.jt808.config.Jt808AutoConfiguration;
import com.lingniu.ingest.protocol.jt808.mapper.Jt808EventMapper;
import com.lingniu.ingest.session.DeviceSession;
import com.lingniu.ingest.session.SessionStore;
import com.lingniu.ingest.session.config.SessionCoreAutoConfiguration;
import com.lingniu.ingest.sink.archive.ArchiveStore;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.UnaryOperator;

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

class Jt1078AutoConfigurationTest {

    private final ApplicationContextRunner runner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(
                    IngestCoreAutoConfiguration.class,
                    Jt1078AutoConfiguration.class,
                    Jt1078SignalAutoConfiguration.class))
            .withUserConfiguration(SupportBeans.class)
            .withPropertyValues(
                    "lingniu.ingest.jt1078.enabled=true",
                    "lingniu.ingest.jt1078.media-stream.enabled=false");

    @Test
    void mediaStreamDefaultsToLegacyProductionPort() {
        Jt1078Properties props = new Jt1078Properties();

        assertThat(props.getMediaStream().getPort()).isEqualTo(11078);
        assertThat(props.getMediaStream().getUdpPort()).isEqualTo(11078);
    }

    @Test
    void mediaArchiveStartsWithoutJt808Mapper() {
        runner.run(context -> {
            assertThat(context).hasSingleBean(Jt1078MediaArchiveService.class);
            assertThat(context).doesNotHaveBean(Jt808EventMapper.class);
            assertThat(context).doesNotHaveBean(Jt1078MediaSignalHandler.class);
        });
    }

    @Test
    void mediaSignalHandlerStartsWhenJt808IsEnabled() {
        new ApplicationContextRunner()
                .withConfiguration(AutoConfigurations.of(
                        Jt1078AutoConfiguration.class,
                        Jt1078SignalAutoConfiguration.class,
                        Jt808AutoConfiguration.class,
                        IngestCoreAutoConfiguration.class,
                        SessionCoreAutoConfiguration.class,
                        VehicleIdentityAutoConfiguration.class))
                .withUserConfiguration(SupportBeans.class)
                .withPropertyValues(
                        "lingniu.ingest.jt1078.enabled=true",
                        "lingniu.ingest.jt1078.media-stream.enabled=false",
                        "lingniu.ingest.jt808.enabled=true",
                        "lingniu.ingest.jt808.port=0")
                .run(context -> {
                    assertThat(context).hasSingleBean(Jt808EventMapper.class);
                    assertThat(context).hasSingleBean(Jt1078MediaSignalHandler.class);
                });
    }

    @Test
    void malformedRtpPublisherUsesConfiguredIdentityResolver() {
        runner.run(context -> {
            Jt1078MalformedRtpEventPublisher publisher = context.getBean(Jt1078MalformedRtpEventPublisher.class);
            CapturingSink sink = context.getBean(CapturingSink.class);

            publisher.publish(new Jt1078MalformedRtpPacket(
                    "tcp",
                    "10.0.0.8:1078",
                    "013812345678",
                    new byte[]{0x30, 0x31, 0x63, 0x64},
                    128,
                    "jt1078 rtp packet too large: 128",
                    java.time.Instant.parse("2026-06-22T08:30:00Z")));

            List<VehicleEvent> events = sink.awaitCount(2);
            assertThat(events).anySatisfy(event -> {
                assertThat(event).isInstanceOf(VehicleEvent.RawArchive.class);
                assertThat(event.vin()).isEqualTo("LNVIN107800000001");
                assertThat(event.metadata())
                        .containsEntry("sim", "013812345678")
                        .containsEntry("identityResolved", "true")
                        .containsEntry("identitySource", "BOUND_PHONE")
                        .containsEntry("parseError", "true");
            });
            assertThat(events).anySatisfy(event -> {
                assertThat(event).isInstanceOf(VehicleEvent.Passthrough.class);
                assertThat(event.vin()).isEqualTo("LNVIN107800000001");
                assertThat(event.metadata())
                        .containsEntry("sim", "013812345678")
                        .containsEntry("identityResolved", "true")
                        .containsEntry("identitySource", "BOUND_PHONE")
                        .containsEntry("parseError", "true")
                        .containsKey("rawArchiveUri");
            });
        });
    }

    @Configuration(proxyBeanMethods = false)
    static class SupportBeans {

        @Bean
        ArchiveStore archiveStore() {
            return new InMemoryArchiveStore();
        }

        @Bean
        VehicleIdentityResolver vehicleIdentityResolver() {
            return lookup -> {
                if (!lookup.vin().isBlank()) {
                    return new VehicleIdentity(lookup.vin(), true, VehicleIdentitySource.EXPLICIT_VIN);
                }
                if ("013812345678".equals(lookup.phone())) {
                    return new VehicleIdentity("LNVIN107800000001", true, VehicleIdentitySource.BOUND_PHONE);
                }
                return new VehicleIdentity("unknown", false, VehicleIdentitySource.UNKNOWN);
            };
        }

        @Bean
        VehicleIdentityRegistry vehicleIdentityRegistry() {
            return binding -> {
            };
        }

        @Bean
        SessionStore sessionStore() {
            return new EmptySessionStore();
        }

        @Bean(destroyMethod = "close")
        DisruptorEventBus disruptorEventBus(CapturingSink sink) {
            return new DisruptorEventBus(1024, "blocking", List.of(sink));
        }

        @Bean
        CapturingSink capturingSink() {
            return new CapturingSink();
        }
    }

    private static final class EmptySessionStore implements SessionStore {
        @Override
        public void put(DeviceSession session) {
        }

        @Override
        public Optional<DeviceSession> findBySessionId(String sessionId) {
            return Optional.empty();
        }

        @Override
        public Optional<DeviceSession> findByVin(String vin) {
            return Optional.empty();
        }

        @Override
        public Optional<DeviceSession> findByPhone(String phone) {
            return Optional.empty();
        }

        @Override
        public Optional<DeviceSession> update(String sessionId, UnaryOperator<DeviceSession> updater) {
            return Optional.empty();
        }

        @Override
        public void remove(String sessionId) {
        }

        @Override
        public int size() {
            return 0;
        }
    }

    private static final class CapturingSink implements EventSink {
        private final CopyOnWriteArrayList<VehicleEvent> events = new CopyOnWriteArrayList<>();

        @Override
        public String name() {
            return "capturing";
        }

        @Override
        public CompletableFuture<Void> publish(VehicleEvent event) {
            events.add(event);
            return CompletableFuture.completedFuture(null);
        }

        VehicleEvent awaitOne() throws InterruptedException {
            for (int i = 0; i < 50; i++) {
                if (!events.isEmpty()) {
                    return events.getFirst();
                }
                Thread.sleep(20);
            }
            throw new AssertionError("expected one event");
        }

        List<VehicleEvent> awaitCount(int count) throws InterruptedException {
            for (int i = 0; i < 50; i++) {
                if (events.size() >= count) {
                    return List.copyOf(events);
                }
                Thread.sleep(20);
            }
            throw new AssertionError("expected " + count + " events, got " + events.size());
        }
    }

    private static final class InMemoryArchiveStore implements ArchiveStore {

        @Override
        public String put(String key, InputStream data, long length) throws IOException {
            data.transferTo(OutputStream.nullOutputStream());
            return "memory://" + key;
        }

        @Override
        public String append(String key, byte[] chunk) {
            return "memory://" + key;
        }

        @Override
        public InputStream get(String key) {
            return new ByteArrayInputStream(new byte[0]);
        }

        @Override
        public boolean exists(String key) {
            return true;
        }

        @Override
        public long size(String key) {
            return 0;
        }
    }
}
