package study.configbus;

import org.junit.jupiter.api.Test;
import org.springframework.boot.builder.SpringApplicationBuilder;
import java.time.Duration;
import java.nio.file.Files;
import java.util.UUID;

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

/** Public observation is HTTP; no direct refresh/event-listener calls. */
class ConfigBusHttpTest {
    @Test
    void clientLoadsGitProfileThroughRealHttpServer() throws Exception {
        try (var fixture = new ConfigFixture()) {
            assertTrue(fixture.get(fixture.clientA, "/state").contains("\"message\":\"profile-v1\""));
            assertTrue(fixture.get(fixture.clientB, "/state").contains("\"message\":\"profile-v1\""));
            System.out.println("U34 initialRead=PASS clients=2 profile=profile-v1");
        }
    }

    @Test
    void gitCommitDoesNotRefreshObjectsUntilTheLocalEndpointRuns() throws Exception {
        try (var fixture = new ConfigFixture()) {
            var before = fixture.get(fixture.clientA, "/state");
            assertTrue(before.contains("\"sticky\":\"profile-v1\""));
            fixture.publish("v2", "profile-v2", 8);
            assertTrue(fixture.get(fixture.clientA, "/state").contains("\"revision\":\"v1\""));
            assertEquals(200, fixture.post(fixture.clientA, "/actuator/refresh"));
            var after = fixture.get(fixture.clientA, "/state");
            assertTrue(after.contains("\"revision\":\"v2\""));
            assertTrue(after.contains("\"message\":\"profile-v2\""));
            assertTrue(after.contains("\"sticky\":\"profile-v1\""));
            assertTrue(fixture.get(fixture.clientB, "/state").contains("\"revision\":\"v1\""));
            System.out.println("U34 localRefresh=PASS gitCommitAlone=v1 refreshedA=v2 untouchedB=v1 sticky=v1");
        }
    }

    @Test
    void busDestinationUpdatesOnlyBAndBroadcastConvergesBoth() throws Exception {
        try (var fixture = new ConfigFixture()) {
            fixture.publish("v2", "profile-v2", 8);
            assertEquals(204, fixture.post(fixture.clientA, "/actuator/busrefresh/catalog:b"));
            fixture.awaitState(fixture.clientB, "\"revision\":\"v2\"");
            assertTrue(fixture.get(fixture.clientA, "/state").contains("\"revision\":\"v1\""));
            fixture.publish("v3", "profile-v3", 12);
            assertEquals(204, fixture.post(fixture.clientA, "/actuator/busrefresh"));
            fixture.awaitState(fixture.clientA, "\"revision\":\"v3\"");
            fixture.awaitState(fixture.clientB, "\"revision\":\"v3\"");
            System.out.println("U34 busPropagation=PASS transport=real-kafka targeted=A:v1,B:v2 broadcast=A:v3,B:v3");
        }
    }

    @Test
    void oneFailedConfigPathLeavesMixedVersionsAndExplicitRefreshRepairsIt() throws Exception {
        try (var fixture = new ConfigFixture()) {
            fixture.gateB.blocked.set(true);
            fixture.publish("v2", "profile-v2", 8);
            assertEquals(204, fixture.post(fixture.clientA, "/actuator/busrefresh"));
            fixture.awaitState(fixture.clientA, "\"revision\":\"v2\"");
            long deadline = System.nanoTime() + Duration.ofSeconds(25).toNanos();
            while (fixture.gateB.failedRequests.get() == 0 && System.nanoTime() < deadline) Thread.sleep(50);
            assertTrue(fixture.gateB.failedRequests.get() > 0, "B must receive the Bus event and attempt Config HTTP");
            assertTrue(fixture.get(fixture.clientB, "/state").contains("\"revision\":\"v1\""));
            fixture.gateB.blocked.set(false);
            assertEquals(200, fixture.post(fixture.clientB, "/actuator/refresh"));
            fixture.awaitState(fixture.clientB, "\"revision\":\"v2\"");
            System.out.println("U34 partialFailure=PASS publishHttp=204 mixed=A:v2,B:v1 repairedB=v2 fault=config-http-503");
        }
    }

    @Test
    void invalidLazyTargetFailsAfterRefreshAndGitRevertRestoresWithANewVersion() throws Exception {
        try (var fixture = new ConfigFixture()) {
            assertTrue(fixture.get(fixture.clientA, "/state").contains("\"revision\":\"v1\""));
            String bad = fixture.publish("bad-v2", "invalid-policy", 0);
            assertEquals(200, fixture.post(fixture.clientA, "/actuator/refresh"));
            assertEquals(500, fixture.request(ConfigFixture.base(fixture.clientA) + "/state", "GET", "", true).statusCode());
            var revert = fixture.git.revert().include(org.eclipse.jgit.lib.ObjectId.fromString(bad)).call();
            assertNotNull(revert);
            assertNotEquals(bad, revert.getName());
            assertEquals(200, fixture.post(fixture.clientA, "/actuator/refresh"));
            String restored = fixture.get(fixture.clientA, "/state");
            assertTrue(restored.contains("\"message\":\"profile-v1\""));
            assertTrue(restored.contains("\"workers\":4"));
            assertTrue(restored.contains("\"configVersion\":\"" + revert.getName() + "\""));
            System.out.println("U34 rollback=PASS refreshHttp=200 invalidBusinessHttp=500 restored=v1 newCommit=true");
        }
    }

    @Test
    void ciphertextAtRestDoesNotReplaceAuthenticationOrEnvironmentRedaction() throws Exception {
        try (var fixture = new ConfigFixture()) {
            String secret = "synthetic-token-" + UUID.randomUUID();
            String serverUrl = ConfigFixture.base(fixture.server);
            assertEquals(401, fixture.request(serverUrl + "/catalog/prod/main", "GET", "", false).statusCode());
            assertEquals(401, fixture.request(serverUrl + "/encrypt", "POST", secret, false).statusCode());
            assertEquals(401, fixture.request(ConfigFixture.base(fixture.clientA) + "/actuator/refresh", "POST", "", false).statusCode());
            var encrypted = fixture.request(serverUrl + "/encrypt", "POST", secret, true);
            assertEquals(200, encrypted.statusCode());
            assertFalse(encrypted.body().contains(secret));
            String content = "lab.secret={cipher}" + encrypted.body().trim() + "\n";
            fixture.commitFile("secrets-prod.properties", content);
            // Application name is catalog: an unrelated app file must not inject this secret.
            assertFalse(fixture.get(fixture.server, "/catalog/prod/main").contains("lab.secret"));
            assertFalse(fixture.get(fixture.clientA, "/state").contains("\"secretPresent\":true"));
            String policy = Files.readString(fixture.repository.resolve("catalog-prod.properties"));
            fixture.commitFile("catalog-prod.properties", policy + content);
            assertFalse(Files.readString(fixture.repository.resolve("catalog-prod.properties")).contains(secret));
            // Authorized Config HTTP returns plaintext by design: encryption protects Git, not transport.
            assertTrue(fixture.get(fixture.server, "/catalog/prod/main").contains(secret));
            assertEquals(200, fixture.post(fixture.clientA, "/actuator/refresh"));
            String state = fixture.get(fixture.clientA, "/state");
            assertTrue(state.contains("\"secretPresent\":true"));
            assertFalse(state.contains(secret));
            String environment = fixture.get(fixture.clientA, "/actuator/env");
            assertTrue(environment.contains("lab.secret"));
            assertFalse(environment.contains(secret));
            fixture.commitFile("catalog-prod.properties", policy + "lab.secret={cipher}not-valid-hex\n");
            String invalid = fixture.get(fixture.server, "/catalog/prod/main");
            assertTrue(invalid.contains("invalid.lab.secret"));
            assertFalse(invalid.contains(secret));
            fixture.commitFile("catalog-prod.properties", policy + content);
            assertEquals(200, fixture.post(fixture.clientA, "/actuator/refresh"));
            assertTrue(fixture.get(fixture.clientA, "/state").contains("\"secretPresent\":true"));
            System.out.println("U34 secretProtection=PASS unauthorized=401 git=cipher authorizedConfig=plaintext env=redacted invalidCipher=invalid-prefix recovered=true");
        }
    }
}
