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

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

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.protobuf.util.JsonFormat;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory;
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

final class MxGatewayFixtureTests {
    @Test
    void valueFixtureCasesExposeNativeProjectionAndRawMetadata() throws Exception {
        JsonArray cases = readFixture("values/value-conversion-cases.json").getAsJsonArray("cases");

        for (var element : cases) {
            JsonObject testCase = element.getAsJsonObject();
            MxValue.Builder builder = MxValue.newBuilder();
            JsonFormat.parser().merge(testCase.getAsJsonObject("value").toString(), builder);
            MxValue value = builder.build();

            assertEquals(testCase.get("expectedKind").getAsString(), lowerCamelKind(value));
            if ("timestamp.utc".equals(testCase.get("id").getAsString())) {
                assertEquals(Instant.parse("2026-01-01T00:00:04Z"), MxValues.nativeValue(value));
            }
            if ("string-array".equals(testCase.get("id").getAsString())) {
                assertEquals(List.of("alpha", "beta"), MxValues.nativeValue(value));
            }
            if ("raw-fallback.variant".equals(testCase.get("id").getAsString())) {
                assertEquals("No lossless typed projection exists for this VARIANT.", value.getRawDiagnostic());
                assertArrayEquals(new byte[] {1, 2, 3, 4, 5}, (byte[]) MxValues.nativeValue(value));
            }
        }
    }

    @Test
    void statusFixtureCasesPreserveRawFields() throws Exception {
        JsonArray cases = readFixture("statuses/status-conversion-cases.json").getAsJsonArray("cases");

        for (var element : cases) {
            JsonObject testCase = element.getAsJsonObject();
            MxStatusProxy.Builder builder = MxStatusProxy.newBuilder();
            JsonFormat.parser().merge(testCase.getAsJsonObject("status").toString(), builder);
            MxStatusProxy status = builder.build();

            assertEquals(
                    testCase.getAsJsonObject("status").get("rawCategory").getAsInt(),
                    status.getRawCategory());
            if ("ok.responding-lmx".equals(testCase.get("id").getAsString())) {
                assertTrue(MxStatuses.succeeded(status));
            }
            if ("security-error.requesting-lmx".equals(testCase.get("id").getAsString())) {
                assertFalse(MxStatuses.succeeded(status));
                assertEquals(MxStatusCategory.MX_STATUS_CATEGORY_SECURITY_ERROR, MxStatuses.view(status).category());
            }
        }
    }

    @Test
    void mxAccessFailureFixtureMapsToRichCommandException() throws Exception {
        MxCommandReply.Builder builder = MxCommandReply.newBuilder();
        JsonFormat.parser().merge(
                Files.readString(fixtureRoot().resolve("command-replies/write.mxaccess-failure.reply.json")),
                builder);
        MxCommandReply reply = builder.build();

        try {
            MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
        } catch (MxAccessException error) {
            assertEquals(ProtocolStatusCode.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE, error.protocolStatus().getCode());
            assertEquals(-2147220992, error.reply().getHresult());
            assertEquals(2, error.reply().getStatusesCount());
            return;
        }

        throw new AssertionError("expected MxAccessException");
    }

    @ParameterizedTest
    @CsvSource({
        "register.ok.reply.json,false",
        "write.status-category-error-success-set.reply.json,true",
        "write.status-category-ok-success-zero.reply.json,false",
        "write.hresult-s-false.reply.json,false",
        "write.hresult-e-fail.reply.json,true",
    })
    void replyValidationFixturesBranchOnCategoryAndNegativeHresult(String fixture, boolean expectFailure)
            throws Exception {
        MxCommandReply.Builder builder = MxCommandReply.newBuilder();
        JsonFormat.parser().merge(
                Files.readString(fixtureRoot().resolve("command-replies/" + fixture)),
                builder);
        MxCommandReply reply = builder.build();

        if (expectFailure) {
            assertThrows(MxAccessException.class, () -> MxGatewayErrors.ensureMxAccessSuccess("write", reply));
        } else {
            MxGatewayErrors.ensureMxAccessSuccess("write", reply);
        }
    }

    @ParameterizedTest
    @CsvSource({
        "MX_STATUS_CATEGORY_OK,0,true",
        "MX_STATUS_CATEGORY_OK,1,true",
        "MX_STATUS_CATEGORY_COMMUNICATION_ERROR,1,false",
        "MX_STATUS_CATEGORY_UNSPECIFIED,1,false",
    })
    void statusEntryVerdictIgnoresTheRawSuccessMember(String category, int success, boolean expectSucceeded) {
        MxStatusProxy status = MxStatusProxy.newBuilder()
                .setCategory(MxStatusCategory.valueOf(category))
                .setSuccess(success)
                .build();

        assertEquals(expectSucceeded, MxStatuses.succeeded(status));
    }

    @Test
    void absentStatusEntryIsSuccess() {
        assertTrue(MxStatuses.succeeded(null));
    }

    @Test
    void grpcAuthErrorsAreClassifiedAndRedacted() {
        RuntimeException authError = MxGatewayErrors.fromGrpc(
                "open session",
                new io.grpc.StatusRuntimeException(io.grpc.Status.UNAUTHENTICATED.withDescription(
                        "invalid API key mxgw_visible_secret")));
        RuntimeException permissionError = MxGatewayErrors.fromGrpc(
                "write",
                new io.grpc.StatusRuntimeException(io.grpc.Status.PERMISSION_DENIED.withDescription(
                        "missing scope mxaccess.write")));

        assertInstanceOf(MxGatewayAuthenticationException.class, authError);
        assertInstanceOf(MxGatewayAuthorizationException.class, permissionError);
        assertTrue(authError.getMessage().contains("<redacted>"));
        assertFalse(authError.getMessage().contains("visible_secret"));
    }

    private static JsonObject readFixture(String relativePath) throws Exception {
        return JsonParser.parseString(Files.readString(fixtureRoot().resolve(relativePath))).getAsJsonObject();
    }

    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 static String lowerCamelKind(MxValue value) {
        String[] parts = value.getKindCase().name().toLowerCase().split("_");
        StringBuilder result = new StringBuilder(parts[0]);
        for (int index = 1; index < parts.length; index++) {
            result.append(Character.toUpperCase(parts[index].charAt(0))).append(parts[index].substring(1));
        }
        return result.toString();
    }
}
