package p.studio.debug.runtime;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import p.studio.StudioBackgroundTasks;
import p.studio.execution.StudioExecutionSessionService;
import p.studio.execution.StudioExecutionState;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.*;

final class StudioRuntimeHandshakeServiceTest {
    @Test
    void connectsPublishesRuntimeHandshakeAndStreamsLogEvents() throws Exception {
        try (final ServerSocket serverSocket = new ServerSocket(0)) {
            final CountDownLatch clientStarted = new CountDownLatch(1);
            final CountDownLatch eventDelivered = new CountDownLatch(1);
            final Thread serverThread = new Thread(() -> serveSuccessfulHandshake(serverSocket, clientStarted, eventDelivered));
            serverThread.start();

            final StudioExecutionSessionService session = new StudioExecutionSessionService();
            session.beginPreparationCycle();
            final StudioRuntimeDebugConnectionSettings settings = new StudioRuntimeDebugConnectionSettings("127.0.0.1", serverSocket.getLocalPort());
            final StudioBackgroundTasks backgroundTasks = new StudioBackgroundTasks(Executors.newSingleThreadExecutor());
            try {
                final StudioRuntimeHandshakeService service = new StudioRuntimeHandshakeService(new ObjectMapper(), backgroundTasks);
                final StudioRuntimeHandshakeResult result = service.connect(session, settings);

                assertTrue(result.success());
                assertEquals(StudioExecutionState.RUNNING, session.snapshot().state());
                assertTrue(clientStarted.await(2, TimeUnit.SECONDS));
                assertTrue(eventDelivered.await(2, TimeUnit.SECONDS));
                assertTrue(session.snapshot().logs().stream().anyMatch(entry -> entry.message().contains("Runtime handshake connected")));
                assertTrue(waitForRuntimeLog(session, "[VM] Boot complete"));

                service.disconnect(session);
            } finally {
                backgroundTasks.shutdown();
                serverThread.join(2_000);
            }
        }
    }

    @Test
    void handshakeFailureTransitionsSessionToRuntimeFailed() {
        final StudioExecutionSessionService session = new StudioExecutionSessionService();
        session.beginPreparationCycle();
        final StudioBackgroundTasks backgroundTasks = new StudioBackgroundTasks(Executors.newSingleThreadExecutor());
        try {
            final StudioRuntimeHandshakeService service = new StudioRuntimeHandshakeService(new ObjectMapper(), backgroundTasks);
            final StudioRuntimeHandshakeResult result = service.connect(
                    session,
                    new StudioRuntimeDebugConnectionSettings("127.0.0.1", 1));

            assertFalse(result.success());
            assertEquals(StudioExecutionState.RUNTIME_FAILED, session.snapshot().state());
            assertTrue(session.snapshot().logs().stream().anyMatch(entry -> entry.message().contains("Runtime handshake failed")));
        } finally {
            backgroundTasks.shutdown();
        }
    }

    private void serveSuccessfulHandshake(
            final ServerSocket serverSocket,
            final CountDownLatch clientStarted,
            final CountDownLatch eventDelivered) {
        try (Socket socket = serverSocket.accept();
             BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
             BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) {
            writer.write("{\"type\":\"handshake\",\"protocol_version\":1,\"runtime_version\":\"1.0.0\"}");
            writer.newLine();
            writer.flush();
            final String clientHandshake = reader.readLine();
            if (!"{\"type\":\"start\"}".equals(clientHandshake)) {
                throw new IllegalStateException("unexpected client handshake: " + clientHandshake);
            }
            clientStarted.countDown();
            writer.write("{\"event\":\"log\",\"level\":\"INFO\",\"source\":\"VM\",\"msg\":\"Boot complete\"}");
            writer.newLine();
            writer.flush();
            eventDelivered.countDown();
        } catch (Exception exception) {
            throw new RuntimeException(exception);
        }
    }

    private boolean waitForRuntimeLog(
            final StudioExecutionSessionService session,
            final String fragment) throws InterruptedException {
        for (int attempt = 0; attempt < 20; attempt += 1) {
            if (session.snapshot().logs().stream().anyMatch(entry -> entry.message().contains(fragment))) {
                return true;
            }
            Thread.sleep(50L);
        }
        return false;
    }
}
