package de.gecheckt.pdf.umbenenner.bootstrap.singleinstance;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.net.ServerSocket;

import org.junit.jupiter.api.Test;

/**
 * Unit-Tests für {@link SingleInstanceGuard}.
 * <p>
 * Alle Tests verwenden einen dynamisch gebundenen freien Port, damit parallele
 * Testläufe keine Konflikte auf dem Produktions-Standardport erzeugen.
 */
class SingleInstanceGuardTest {

    /**
     * Ermittelt einen freien lokalen Port.
     *
     * @return ein freier TCP-Port
     * @throws Exception wenn kein freier Port ermittelt werden kann
     */
    private static int freienPortErmitteln() throws Exception {
        try (ServerSocket s = new ServerSocket(0)) {
            return s.getLocalPort();
        }
    }

    @Test
    void ersterAcquireErfolgtOhneFehler() throws Exception {
        int port = freienPortErmitteln();
        try (SingleInstanceGuard guard = new SingleInstanceGuard(port)) {
            assertDoesNotThrow(guard::acquire,
                    "Der erste acquire() soll den Port erfolgreich belegen.");
        }
    }

    @Test
    void zweiterAcquireWirftAnotherInstanceRunningException() throws Exception {
        int port = freienPortErmitteln();
        try (SingleInstanceGuard ersterGuard = new SingleInstanceGuard(port);
             SingleInstanceGuard zweiterGuard = new SingleInstanceGuard(port)) {
            ersterGuard.acquire();

            assertThrows(AnotherInstanceRunningException.class, zweiterGuard::acquire,
                    "Während der erste Guard aktiv ist, soll der zweite acquire() eine "
                            + "AnotherInstanceRunningException werfen.");
        }
    }

    @Test
    void closeErlaubtNeuesBinden() throws Exception {
        int port = freienPortErmitteln();
        SingleInstanceGuard ersterGuard = new SingleInstanceGuard(port);
        ersterGuard.acquire();
        ersterGuard.close();

        // Nach close() soll ein neuer Guard den Port wieder belegen können
        try (SingleInstanceGuard neuerGuard = new SingleInstanceGuard(port)) {
            assertDoesNotThrow(neuerGuard::acquire,
                    "Nach close() des ersten Guards soll ein neuer Guard erfolgreich binden können.");
        }
    }

    @Test
    void closeIstIdempotent() throws Exception {
        int port = freienPortErmitteln();
        SingleInstanceGuard guard = new SingleInstanceGuard(port);
        guard.acquire();

        assertDoesNotThrow(() -> {
            guard.close();
            guard.close();
            guard.close();
        }, "Mehrfaches close() soll keine Ausnahme werfen.");
    }
}
