package com.zb.mom.ww.mxgateway.client;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.protobuf.util.JsonFormat;
import io.grpc.ManagedChannel;
import io.grpc.Server;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.inprocess.InProcessServerBuilder;
import io.grpc.stub.StreamObserver;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.UUID;
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import org.junit.jupiter.api.Test;

final class MxGatewayCredentialReplyTests {
    private static final String CREDENTIAL = "sup3rSecretVerify9f3a2b";

    @Test
    void authenticateUserRedactsEchoedCredentialFromReplyDrivenError() throws Exception {
        assertCredentialFullyRedacted(
                "authenticate-user.echoed-credential.reply.json", "auth-echo-session");
    }

    @Test
    void authenticateUserRedactsEchoedCredentialFromMxAccessFailureReply() throws Exception {
        assertCredentialFullyRedacted(
                "authenticate-user.echoed-credential-mxaccess-failure.reply.json",
                "auth-echo-failure-session");
    }

    private static void assertCredentialFullyRedacted(String fixture, String sessionId) throws Exception {
        MxCommandReply reply = loadReply(fixture);

        try (InProcessGateway gateway = InProcessGateway.startReturning(reply);
                MxGatewayClient client = gateway.client()) {
            MxGatewaySession session = MxGatewaySession.forSessionId(client, sessionId);

            MxAccessException error = assertThrows(
                    MxAccessException.class,
                    () -> session.authenticateUser(12, "operator", CREDENTIAL));

            assertFalse(error.getMessage().contains(CREDENTIAL),
                    "credential echoed by the gateway must not survive in the surfaced message");
            assertTrue(error.getMessage().contains("<redacted>"),
                    "the echoed credential must be replaced with the redaction marker");

            // The rebuilt exception must not re-expose the credential through the
            // structured reply/protocolStatus a caller can inspect and log.
            MxCommandReply surfaced = error.reply();
            assertNotNull(surfaced, "the redacted exception must preserve a reply for inspection");
            assertFalse(surfaced.getProtocolStatus().getMessage().contains(CREDENTIAL),
                    "reply protocol status message must not leak the echoed credential");
            assertFalse(surfaced.getDiagnosticMessage().contains(CREDENTIAL),
                    "reply diagnostic message must not leak the echoed credential");
            for (int index = 0; index < surfaced.getStatusesCount(); index++) {
                assertFalse(surfaced.getStatusesList().get(index).getDiagnosticText().contains(CREDENTIAL),
                        "reply status diagnostic text must not leak the echoed credential");
            }
            assertNotNull(error.protocolStatus(), "the redacted exception must preserve a protocol status");
            assertFalse(error.protocolStatus().getMessage().contains(CREDENTIAL),
                    "exception protocol status must not leak the echoed credential");
        }
    }

    @Test
    void authenticateUserMissingPayloadThrowsMalformedReply() throws Exception {
        MxCommandReply reply = loadReply("authenticate-user.missing-payload.reply.json");

        try (InProcessGateway gateway = InProcessGateway.startReturning(reply);
                MxGatewayClient client = gateway.client()) {
            MxGatewaySession session = MxGatewaySession.forSessionId(client, "auth-missing-session");

            assertThrows(
                    MxGatewayMalformedReplyException.class,
                    () -> session.authenticateUser(3, "operator", "pw"));
        }
    }

    @Test
    void authenticateUserReturnValueOnlyReplyReturnsInt32Fallback() throws Exception {
        MxCommandReply reply = loadReply("authenticate-user.return-value-only.reply.json");

        try (InProcessGateway gateway = InProcessGateway.startReturning(reply);
                MxGatewayClient client = gateway.client()) {
            MxGatewaySession session = MxGatewaySession.forSessionId(client, "auth-return-session");

            assertEquals(7, session.authenticateUser(3, "operator", "pw"));
        }
    }

    @Test
    void addBufferedItemReturnValueOnlyReplyReturnsInt32Fallback() throws Exception {
        MxCommandReply reply = MxCommandReply.newBuilder()
                .setProtocolStatus(ok())
                .setReturnValue(MxValue.newBuilder().setInt32Value(55))
                .build();

        try (InProcessGateway gateway = InProcessGateway.startReturning(reply);
                MxGatewayClient client = gateway.client()) {
            MxGatewaySession session = MxGatewaySession.forSessionId(client, "buffered-return-session");

            assertEquals(55, session.addBufferedItem(3, "Tank01.Level", "galaxy"));
        }
    }

    @Test
    void addBufferedItemMissingPayloadThrowsMalformedReply() throws Exception {
        MxCommandReply reply = MxCommandReply.newBuilder().setProtocolStatus(ok()).build();

        try (InProcessGateway gateway = InProcessGateway.startReturning(reply);
                MxGatewayClient client = gateway.client()) {
            MxGatewaySession session = MxGatewaySession.forSessionId(client, "buffered-malformed-session");

            assertThrows(
                    MxGatewayMalformedReplyException.class,
                    () -> session.addBufferedItem(3, "Tank01.Level", "galaxy"));
        }
    }

    private static ProtocolStatus ok() {
        return ProtocolStatus.newBuilder()
                .setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
                .build();
    }

    private static MxCommandReply loadReply(String fixture) throws Exception {
        MxCommandReply.Builder builder = MxCommandReply.newBuilder();
        JsonFormat.parser().merge(
                Files.readString(fixtureRoot().resolve("command-replies/" + fixture)),
                builder);
        return builder.build();
    }

    private static Path fixtureRoot() {
        Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath();
        for (Path path = current; path != null; path = path.getParent()) {
            Path candidate = path.resolve("clients/proto/fixtures/behavior");
            if (Files.exists(candidate)) {
                return candidate;
            }
            candidate = path.resolve("../proto/fixtures/behavior").normalize();
            if (Files.exists(candidate)) {
                return candidate;
            }
        }
        throw new IllegalStateException("could not locate behavior fixtures from " + current);
    }

    private record InProcessGateway(Server server, ManagedChannel channel) implements AutoCloseable {
        static InProcessGateway startReturning(MxCommandReply reply) throws Exception {
            String serverName = "mxgw-java-cred-" + UUID.randomUUID();
            MxAccessGatewayGrpc.MxAccessGatewayImplBase service =
                    new MxAccessGatewayGrpc.MxAccessGatewayImplBase() {
                        @Override
                        public void invoke(
                                MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
                            responseObserver.onNext(reply);
                            responseObserver.onCompleted();
                        }
                    };
            Server server = InProcessServerBuilder.forName(serverName)
                    .directExecutor()
                    .addService(service)
                    .build()
                    .start();
            ManagedChannel channel = InProcessChannelBuilder.forName(serverName)
                    .directExecutor()
                    .build();
            return new InProcessGateway(server, channel);
        }

        MxGatewayClient client() {
            return new MxGatewayClient(
                    channel,
                    MxGatewayClientOptions.builder()
                            .endpoint("in-process")
                            .apiKey("")
                            .plaintext(true)
                            .callTimeout(Duration.ofSeconds(5))
                            .build());
        }

        @Override
        public void close() {
            channel.shutdownNow();
            server.shutdownNow();
        }
    }
}
