package vassistent.service;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

/**
 * Unit tests for {@link ProcessManagerService} using an injected process launcher.
 */
@ExtendWith(MockitoExtension.class)
class ProcessManagerServiceTest {

    @TempDir
    Path tempDir;

    @Mock
    private Process process;

    private Properties config;
    private List<List<String>> launchedCommands;

    /**
     * Initializes test configuration and command capture.
     *
     * @throws Exception if process mock setup fails
     */
    @BeforeEach
    void setUp() throws Exception {
        config = new Properties();
        launchedCommands = new ArrayList<>();
        config.setProperty("process.startup.healthcheck.enabled", "false");
    }

    /**
     * Creates a service instance with command capture and test PID files.
     *
     * @return configured process manager service
     */
    private ProcessManagerService createService() {
        ProcessManagerService.ProcessLauncher launcher = processBuilder -> {
            launchedCommands.add(new ArrayList<>(processBuilder.command()));
            return process;
        };

        return new ProcessManagerService(
                config,
                launcher,
                tempDir.resolve("unreal.pid").toString(),
                tempDir.resolve("signalling.pid").toString()
        );
    }

    /**
     * Stubs process wait behavior for taskkill command execution.
     *
     * @throws Exception if mock stubbing fails
     */
    private void stubProcessWaitFor() throws Exception {
        when(process.waitFor()).thenReturn(0);
    }

    /**
     * Stubs process metadata needed by shutdown of tracked processes.
     *
     * @throws Exception if mock stubbing fails
     */
    private void stubProcessForTrackedShutdown() throws Exception {
        stubProcessWaitFor();
        when(process.pid()).thenReturn(123L);
    }

    /**
     * Enables startup health checks with short polling and timeout for unit tests.
     */
    private void enableFastStartupHealthcheck() {
        config.setProperty("process.startup.healthcheck.enabled", "true");
        config.setProperty("process.startup.healthcheck.timeout.millis", "20");
        config.setProperty("process.startup.healthcheck.poll.millis", "1");
    }

    /**
     * Verifies that no processes are launched when all features are disabled.
     */
    @Test
    void startProcesses_whenDisabled_launchesNothing() {
        config.setProperty("mqtt_sim.enabled", "false");
        config.setProperty("unreal.enabled", "false");

        ProcessManagerService service = createService();
        ProcessManagerService.StartupReport report = service.startProcesses();

        assertTrue(launchedCommands.isEmpty());
        assertNull(service.getPythonProcess());
        assertNull(service.getUnrealProcess());
        assertFalse(report.hasFailures());
        assertFalse(report.getMqttSimulator().isEnabled());
        assertFalse(report.getUnreal().isEnabled());
    }

    /**
     * Verifies MQTT simulator launch command when simulator is enabled.
     */
    @Test
    void startProcesses_mqttEnabled_launchesPythonSimulator() {
        config.setProperty("mqtt_sim.enabled", "true");
        config.setProperty("mqtt_sim.script", "sim.py");
        config.setProperty("python.path", "python");
        config.setProperty("unreal.enabled", "false");

        ProcessManagerService service = createService();
        ProcessManagerService.StartupReport report = service.startProcesses();

        assertEquals(1, launchedCommands.size());
        assertEquals(List.of("python", "sim.py"), launchedCommands.get(0));
        assertSame(process, service.getPythonProcess());
        assertFalse(report.hasFailures());
        assertTrue(report.getMqttSimulator().isStarted());
        assertFalse(report.getUnreal().isEnabled());
    }

    /**
     * Verifies Unreal launch command when Unreal integration is enabled.
     */
    @Test
    void startProcesses_unrealEnabled_launchesPowerShell() {
        config.setProperty("mqtt_sim.enabled", "false");
        config.setProperty("unreal.enabled", "true");
        config.setProperty("unreal.executable", "avatar.ps1");
        config.setProperty("unreal.signalling_server.script", "start_signal.bat");

        ProcessManagerService service = createService();
        ProcessManagerService.StartupReport report = service.startProcesses();

        assertEquals(1, launchedCommands.size());
        assertEquals(
                List.of("powershell.exe", "-ExecutionPolicy", "Bypass", "-File", "avatar.ps1"),
                launchedCommands.get(0)
        );
        assertTrue(
                launchedCommands.stream()
                        .noneMatch(command -> !command.isEmpty() && "cmd".equalsIgnoreCase(command.get(0)))
        );
        assertSame(process, service.getUnrealProcess());
        assertFalse(report.hasFailures());
        assertTrue(report.getUnreal().isStarted());
        assertFalse(report.getMqttSimulator().isEnabled());
    }

    /**
     * Verifies Unreal startup enables merged error stream before process launch.
     */
    @Test
    void startProcesses_unrealEnabled_redirectsErrorBeforeLaunch() {
        config.setProperty("mqtt_sim.enabled", "false");
        config.setProperty("unreal.enabled", "true");
        config.setProperty("unreal.executable", "avatar.ps1");

        final boolean[] redirectEnabledAtLaunch = new boolean[1];

        ProcessManagerService.ProcessLauncher launcher = processBuilder -> {
            redirectEnabledAtLaunch[0] = processBuilder.redirectErrorStream();
            return process;
        };

        ProcessManagerService service = new ProcessManagerService(
                config,
                launcher,
                tempDir.resolve("unreal.pid").toString(),
                tempDir.resolve("signalling.pid").toString()
        );

        ProcessManagerService.StartupReport report = service.startProcesses();

        assertFalse(report.hasFailures());
        assertTrue(redirectEnabledAtLaunch[0]);
    }

    /**
     * Verifies MQTT startup health check reports failure when process is not alive.
     */
    @Test
    void startProcesses_mqttHealthcheckFails_reportsFailure() {
        enableFastStartupHealthcheck();
        config.setProperty("mqtt_sim.enabled", "true");
        config.setProperty("mqtt_sim.script", "sim.py");
        config.setProperty("python.path", "python");
        config.setProperty("unreal.enabled", "false");
        when(process.isAlive()).thenReturn(false);

        ProcessManagerService service = createService();
        ProcessManagerService.StartupReport report = service.startProcesses();

        assertTrue(report.hasFailures());
        assertTrue(report.getMqttSimulator().hasFailed());
        assertTrue(report.getMqttSimulator().getErrorMessage().contains("health check"));
    }

    /**
     * Verifies Unreal startup health check fails when PID files are missing.
     */
    @Test
    void startProcesses_unrealHealthcheckMissingPidFiles_reportsFailure() {
        enableFastStartupHealthcheck();
        config.setProperty("mqtt_sim.enabled", "false");
        config.setProperty("unreal.enabled", "true");
        config.setProperty("unreal.executable", "avatar.ps1");

        ProcessManagerService service = createService();
        ProcessManagerService.StartupReport report = service.startProcesses();

        assertTrue(report.hasFailures());
        assertTrue(report.getUnreal().hasFailed());
        assertTrue(report.getUnreal().getErrorMessage().contains("health check"));
    }

    /**
     * Verifies Unreal startup health check succeeds when PID files contain alive process IDs.
     *
     * @throws Exception if PID files cannot be created
     */
    @Test
    void startProcesses_unrealHealthcheckWithAlivePidFiles_startsSuccessfully() throws Exception {
        enableFastStartupHealthcheck();
        config.setProperty("mqtt_sim.enabled", "false");
        config.setProperty("unreal.enabled", "true");
        config.setProperty("unreal.executable", "avatar.ps1");

        Path unrealPid = tempDir.resolve("unreal.pid");
        Path signallingPid = tempDir.resolve("signalling.pid");
        long currentPid = ProcessHandle.current().pid();

        ProcessManagerService.ProcessLauncher launcher = processBuilder -> {
            launchedCommands.add(new ArrayList<>(processBuilder.command()));
            Files.writeString(unrealPid, String.valueOf(currentPid));
            Files.writeString(signallingPid, String.valueOf(currentPid));
            return process;
        };

        ProcessManagerService service = new ProcessManagerService(
                config,
                launcher,
                unrealPid.toString(),
                signallingPid.toString()
        );

        ProcessManagerService.StartupReport report = service.startProcesses();

        assertFalse(report.hasFailures());
        assertTrue(report.getUnreal().isStarted());
    }

    /**
     * Verifies shutdown issues taskkill commands for tracked process handles.
     */
    @Test
    void shutdown_withTrackedProcesses_issuesTaskkillCommands() throws Exception {
        stubProcessForTrackedShutdown();

        config.setProperty("mqtt_sim.enabled", "true");
        config.setProperty("mqtt_sim.script", "sim.py");
        config.setProperty("python.path", "python");
        config.setProperty("unreal.enabled", "true");
        config.setProperty("unreal.executable", "avatar.ps1");
        config.setProperty("unreal.signalling_server.script", "start_signal.bat");

        ProcessManagerService service = createService();
        ProcessManagerService.StartupReport report = service.startProcesses();
        service.shutdown();

        long taskkillCount = launchedCommands.stream()
                .filter(command -> !command.isEmpty() && "taskkill".equals(command.get(0)))
                .count();

        assertEquals(2, taskkillCount);
        assertFalse(report.hasFailures());
    }

    /**
     * Verifies shutdown uses PID files when present.
     *
     * @throws Exception if temporary PID files cannot be written
     */
    @Test
    void shutdown_withPidFiles_issuesPidTaskkillCommands() throws Exception {
        stubProcessWaitFor();

        Path unrealPid = tempDir.resolve("unreal.pid");
        Path signallingPid = tempDir.resolve("signalling.pid");
        Files.writeString(unrealPid, "777");
        Files.writeString(signallingPid, "888");

        ProcessManagerService.ProcessLauncher launcher = processBuilder -> {
            launchedCommands.add(new ArrayList<>(processBuilder.command()));
            return process;
        };

        ProcessManagerService service = new ProcessManagerService(
                config,
                launcher,
                unrealPid.toString(),
                signallingPid.toString()
        );

        service.shutdown();

        assertTrue(launchedCommands.stream().anyMatch(command -> command.contains("777")));
        assertTrue(launchedCommands.stream().anyMatch(command -> command.contains("888")));
    }

    /**
     * Verifies a failed simulator launch is reported as startup failure.
     */
    @Test
    void startProcesses_mqttLaunchFails_reportsFailure() {
        config.setProperty("mqtt_sim.enabled", "true");
        config.setProperty("mqtt_sim.script", "sim.py");
        config.setProperty("python.path", "python");
        config.setProperty("unreal.enabled", "false");

        ProcessManagerService.ProcessLauncher failingLauncher = processBuilder -> {
            throw new java.io.IOException("python not found");
        };

        ProcessManagerService service = new ProcessManagerService(
                config,
                failingLauncher,
                tempDir.resolve("unreal.pid").toString(),
                tempDir.resolve("signalling.pid").toString()
        );

        ProcessManagerService.StartupReport report = service.startProcesses();

        assertTrue(report.hasFailures());
        assertTrue(report.getMqttSimulator().hasFailed());
        assertTrue(report.buildFailureMessage().contains("mqtt_sim"));
        assertTrue(report.buildFailureMessage().contains("python not found"));
    }

    /**
     * Verifies a failed Unreal launch is reported as startup failure.
     */
    @Test
    void startProcesses_unrealLaunchFails_reportsFailure() {
        config.setProperty("mqtt_sim.enabled", "false");
        config.setProperty("unreal.enabled", "true");
        config.setProperty("unreal.executable", "avatar.ps1");

        ProcessManagerService.ProcessLauncher failingLauncher = processBuilder -> {
            throw new java.io.IOException("powershell unavailable");
        };

        ProcessManagerService service = new ProcessManagerService(
                config,
                failingLauncher,
                tempDir.resolve("unreal.pid").toString(),
                tempDir.resolve("signalling.pid").toString()
        );

        ProcessManagerService.StartupReport report = service.startProcesses();

        assertTrue(report.hasFailures());
        assertTrue(report.getUnreal().hasFailed());
        assertTrue(report.buildFailureMessage().contains("unreal"));
        assertTrue(report.buildFailureMessage().contains("powershell unavailable"));
    }
}
