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

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

import com.zb.mom.ww.mxgateway.client.MxGatewayAlarmFeedSubscription;
import io.grpc.stub.StreamObserver;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
import mxaccess_gateway.v1.MxaccessGateway.AddItemReply;
import mxaccess_gateway.v1.MxaccessGateway.AlarmConditionState;
import mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage;
import mxaccess_gateway.v1.MxaccessGateway.AlarmTransitionKind;
import mxaccess_gateway.v1.MxaccessGateway.BulkReadResult;
import mxaccess_gateway.v1.MxaccessGateway.BulkWriteResult;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
import mxaccess_gateway.v1.MxaccessGateway.OnAlarmTransitionEvent;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import mxaccess_gateway.v1.MxaccessGateway.RegisterReply;
import mxaccess_gateway.v1.MxaccessGateway.SessionState;
import mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest;
import mxaccess_gateway.v1.MxaccessGateway.SubscribeResult;
import mxaccess_gateway.v1.MxaccessGateway.Write2BulkEntry;
import mxaccess_gateway.v1.MxaccessGateway.WriteBulkEntry;
import mxaccess_gateway.v1.MxaccessGateway.WriteSecured2BulkEntry;
import mxaccess_gateway.v1.MxaccessGateway.WriteSecuredBulkEntry;
import org.junit.jupiter.api.Test;

final class MxGatewayCliTests {
    @Test
    void versionCommandPrintsProtocolVersions() {
        CliRun run = execute(new FakeClientFactory(), "version");

        assertEquals(0, run.exitCode());
        assertEquals("", run.errors());
        assertTrue(run.output().contains("mxgateway-java 0.1.0"));
        assertTrue(run.output().contains("gatewayProtocolVersion=3"));
        assertTrue(run.output().contains("workerProtocolVersion=1"));
    }

    @Test
    void versionCommandPrintsJson() {
        CliRun run = execute(new FakeClientFactory(), "version", "--json");

        assertEquals(0, run.exitCode());
        assertTrue(run.output().contains("\"clientVersion\":\"0.1.0\""));
        assertTrue(run.output().contains("\"gatewayProtocolVersion\":3"));
    }

    @Test
    void openSessionJsonRedactsApiKey() {
        CliRun run = execute(
                new FakeClientFactory(),
                "open-session",
                "--endpoint",
                "localhost:5000",
                "--api-key",
                "mxgw_visible_secret",
                "--plaintext",
                "--client-session-name",
                "java-cli",
                "--json");

        assertEquals(0, run.exitCode());
        assertTrue(run.output().contains("\"command\":\"open-session\""));
        assertTrue(run.output().contains("\"sessionId\":\"session-cli\""));
        assertTrue(run.output().contains("mxgw***********cret"));
        assertFalse(run.output().contains("visible_secret"));
    }

    @Test
    void writeBuildsTypedValueFromParserOptions() {
        FakeClientFactory factory = new FakeClientFactory();
        CliRun run = execute(
                factory,
                "write",
                "--session-id",
                "session-cli",
                "--server-handle",
                "12",
                "--item-handle",
                "34",
                "--type",
                "int32",
                "--value",
                "123",
                "--json");

        assertEquals(0, run.exitCode());
        assertEquals(123, factory.client.session.lastWriteValue.getInt32Value());
        assertTrue(run.output().contains("\"kind\":\"MX_COMMAND_KIND_WRITE\""));
    }

    @Test
    void smokeCommandRunsOpenRegisterAddAdviseAndClose() {
        FakeClientFactory factory = new FakeClientFactory();
        CliRun run = execute(factory, "smoke", "--item", "TestObject.TestInt", "--json");

        assertEquals(0, run.exitCode());
        assertTrue(factory.client.session.registerCalled);
        assertTrue(factory.client.session.addItemCalled);
        assertTrue(factory.client.session.adviseCalled);
        assertTrue(factory.client.closeCalled);
        assertTrue(run.output().contains("\"serverHandle\":42"));
        assertTrue(run.output().contains("\"itemHandle\":7"));
    }

    @Test
    void subscribeBulkCommandPrintsResults() {
        CliRun run = execute(
                new FakeClientFactory(),
                "subscribe-bulk",
                "--session-id",
                "session-cli",
                "--server-handle",
                "42",
                "--items",
                "TestMachine_001.TestChangingInt,TestMachine_002.TestChangingInt",
                "--json");

        assertEquals(0, run.exitCode());
        assertTrue(run.output().contains("\"command\":\"subscribe-bulk\""));
        assertTrue(run.output().contains("\"itemHandle\":100"));
        assertTrue(run.output().contains("\"tagAddress\":\"TestMachine_002.TestChangingInt\""));
    }

    @Test
    void unsubscribeBulkCommandPrintsResults() {
        CliRun run = execute(
                new FakeClientFactory(),
                "unsubscribe-bulk",
                "--session-id",
                "session-cli",
                "--server-handle",
                "42",
                "--item-handles",
                "100,101",
                "--json");

        assertEquals(0, run.exitCode());
        assertTrue(run.output().contains("\"command\":\"unsubscribe-bulk\""));
        assertTrue(run.output().contains("\"itemHandle\":101"));
        assertTrue(run.output().contains("\"wasSuccessful\":true"));
    }

    // ---- stream-alarms / acknowledge-alarm subcommands ----

    @Test
    void streamAlarmsCommandForwardsFilterPrefixAndPrintsFeedMessages() {
        FakeClientFactory factory = new FakeClientFactory();
        CliRun run = execute(factory, "stream-alarms", "--filter-prefix", "Tank01");

        assertEquals(0, run.exitCode());
        assertEquals("Tank01", factory.client.lastStreamAlarmsRequest.getAlarmFilterPrefix());
        String out = run.output();
        assertTrue(out.contains("active-alarm Tank01.Level.HiHi"), out);
        assertTrue(out.contains("snapshot-complete"), out);
        assertTrue(out.contains("transition Tank01.Level.HiHi"), out);
    }

    @Test
    void streamAlarmsCommandHonoursLimit() {
        FakeClientFactory factory = new FakeClientFactory();
        CliRun run = execute(factory, "stream-alarms", "--limit", "1");

        assertEquals(0, run.exitCode());
        long lines = run.output().lines().filter(line -> !line.isBlank()).count();
        assertEquals(1, lines, "expected exactly one feed message with --limit 1, got: " + run.output());
    }

    @Test
    void streamAlarmsCommandPrintsJson() {
        FakeClientFactory factory = new FakeClientFactory();
        CliRun run = execute(factory, "stream-alarms", "--json");

        assertEquals(0, run.exitCode());
        assertTrue(run.output().contains("\"activeAlarm\""), run.output());
        assertTrue(run.output().contains("\"snapshotComplete\""), run.output());
    }

    @Test
    void acknowledgeAlarmCommandForwardsOptionsAndPrintsReply() {
        FakeClientFactory factory = new FakeClientFactory();
        CliRun run = execute(
                factory,
                "acknowledge-alarm",
                "--reference",
                "Tank01.Level.HiHi",
                "--comment",
                "checked",
                "--operator",
                "operator1",
                "--json");

        assertEquals(0, run.exitCode());
        assertEquals("Tank01.Level.HiHi", factory.client.lastAcknowledgeAlarmRequest.getAlarmFullReference());
        assertEquals("checked", factory.client.lastAcknowledgeAlarmRequest.getComment());
        assertEquals("operator1", factory.client.lastAcknowledgeAlarmRequest.getOperatorUser());
        assertTrue(run.output().contains("\"command\":\"acknowledge-alarm\""), run.output());
    }

    @Test
    void acknowledgeAlarmCommandRequiresReference() {
        CliRun run = execute(new FakeClientFactory(), "acknowledge-alarm", "--comment", "checked");

        assertFalse(run.exitCode() == 0, "expected non-zero exit without --reference");
        assertTrue(run.errors().contains("--reference"), run.errors());
    }

    @Test
    void batchCommandExecutesVersionAndEmitsEorMarker() {
        CliRun run = executeBatch(new FakeClientFactory(), "version --json\n");

        assertEquals(0, run.exitCode());
        String out = run.output();
        assertTrue(out.contains("\"clientVersion\""), out);
        assertTrue(out.contains(MxGatewayCli.BATCH_EOR), out);
    }

    @Test
    void batchCommandEmitsEorAfterFailedCommandAndContinues() {
        // An unknown subcommand causes a picocli parse error (non-zero exit).
        // The loop must still emit BATCH_EOR for the failure and continue
        // processing the subsequent valid command.
        CliRun run = executeBatch(new FakeClientFactory(), "no-such-subcommand\nversion --json\n");

        assertEquals(0, run.exitCode());
        String out = run.output();
        long eorCount = out.lines()
                .filter(l -> l.equals(MxGatewayCli.BATCH_EOR))
                .count();
        assertEquals(2, eorCount, "expected exactly 2 EOR sentinels, got: " + eorCount + "\nOutput:\n" + out);
        assertTrue(out.contains("\"clientVersion\""), out);
    }

    /**
     * Runs the CLI with {@code batch} as the subcommand, using the provided
     * string as standard input content. Temporarily replaces {@link System#in}
     * for the duration of the call.
     */
    private static CliRun executeBatch(MxGatewayCli.MxGatewayCliClientFactory factory, String stdinContent) {
        InputStream originalIn = System.in;
        try {
            System.setIn(new ByteArrayInputStream(stdinContent.getBytes(StandardCharsets.UTF_8)));
            return execute(factory, "batch");
        } finally {
            System.setIn(originalIn);
        }
    }

    private static CliRun execute(MxGatewayCli.MxGatewayCliClientFactory factory, String... args) {
        StringWriter output = new StringWriter();
        StringWriter errors = new StringWriter();
        int exitCode = MxGatewayCli.execute(
                factory,
                new PrintWriter(output, true),
                new PrintWriter(errors, true),
                args);
        return new CliRun(exitCode, output.toString(), errors.toString());
    }

    private record CliRun(int exitCode, String output, String errors) {
    }

    private static final class FakeClientFactory implements MxGatewayCli.MxGatewayCliClientFactory {
        private FakeClient client;

        @Override
        public MxGatewayCli.MxGatewayCliClient connect(MxGatewayCli.CommonOptions options) {
            client = new FakeClient(options.spec.commandLine().getOut());
            return client;
        }
    }

    private static final class FakeClient implements MxGatewayCli.MxGatewayCliClient {
        private final PrintWriter out;
        private final FakeSession session = new FakeSession();
        private boolean closeCalled;
        private AcknowledgeAlarmRequest lastAcknowledgeAlarmRequest;
        private StreamAlarmsRequest lastStreamAlarmsRequest;

        private FakeClient(PrintWriter out) {
            this.out = out;
        }

        @Override
        public PrintWriter out() {
            return out;
        }

        @Override
        public OpenSessionReply openSession(OpenSessionRequest request) {
            return OpenSessionReply.newBuilder()
                    .setSessionId("session-cli")
                    .setProtocolStatus(ok())
                    .build();
        }

        @Override
        public CloseSessionReply closeSession(CloseSessionRequest request) {
            closeCalled = true;
            return CloseSessionReply.newBuilder()
                    .setSessionId(request.getSessionId())
                    .setFinalState(SessionState.SESSION_STATE_CLOSED)
                    .setProtocolStatus(ok())
                    .build();
        }

        @Override
        public MxGatewayCli.MxGatewayCliSession session(String sessionId) {
            return session;
        }

        @Override
        public AcknowledgeAlarmReply acknowledgeAlarm(AcknowledgeAlarmRequest request) {
            lastAcknowledgeAlarmRequest = request;
            return AcknowledgeAlarmReply.newBuilder()
                    .setCorrelationId(request.getClientCorrelationId())
                    .setProtocolStatus(ok())
                    .setHresult(0)
                    .build();
        }

        @Override
        public MxGatewayAlarmFeedSubscription streamAlarms(
                StreamAlarmsRequest request, StreamObserver<AlarmFeedMessage> observer) {
            lastStreamAlarmsRequest = request;
            // Replay a deterministic active-alarm snapshot, snapshot-complete
            // sentinel, transition, then complete the feed so the CLI command
            // drains a bounded stream without contacting a live gateway.
            observer.onNext(AlarmFeedMessage.newBuilder()
                    .setActiveAlarm(ActiveAlarmSnapshot.newBuilder()
                            .setAlarmFullReference("Tank01.Level.HiHi")
                            .setCurrentState(AlarmConditionState.ALARM_CONDITION_STATE_ACTIVE)
                            .setSeverity(700))
                    .build());
            observer.onNext(AlarmFeedMessage.newBuilder().setSnapshotComplete(true).build());
            observer.onNext(AlarmFeedMessage.newBuilder()
                    .setTransition(OnAlarmTransitionEvent.newBuilder()
                            .setAlarmFullReference("Tank01.Level.HiHi")
                            .setTransitionKind(AlarmTransitionKind.ALARM_TRANSITION_KIND_ACKNOWLEDGE)
                            .setSeverity(700))
                    .build());
            observer.onCompleted();
            return new MxGatewayAlarmFeedSubscription();
        }

        @Override
        public void close() {
        }
    }

    private static final class FakeSession implements MxGatewayCli.MxGatewayCliSession {
        private boolean registerCalled;
        private boolean addItemCalled;
        private boolean adviseCalled;
        private MxValue lastWriteValue;

        @Override
        public int register(String clientName) {
            registerCalled = true;
            return 42;
        }

        @Override
        public MxCommandReply registerRaw(String clientName) {
            registerCalled = true;
            return MxCommandReply.newBuilder()
                    .setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
                    .setProtocolStatus(ok())
                    .setRegister(RegisterReply.newBuilder().setServerHandle(42))
                    .build();
        }

        @Override
        public int addItem(int serverHandle, String itemDefinition) {
            addItemCalled = true;
            return 7;
        }

        @Override
        public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
            addItemCalled = true;
            return MxCommandReply.newBuilder()
                    .setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
                    .setProtocolStatus(ok())
                    .setAddItem(AddItemReply.newBuilder().setItemHandle(7))
                    .build();
        }

        @Override
        public void advise(int serverHandle, int itemHandle) {
            adviseCalled = true;
        }

        @Override
        public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
            adviseCalled = true;
            return MxCommandReply.newBuilder()
                    .setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
                    .setProtocolStatus(ok())
                    .build();
        }

        @Override
        public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
            lastWriteValue = value;
            return MxCommandReply.newBuilder()
                    .setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
                    .setProtocolStatus(ok())
                    .build();
        }

        @Override
        public List<SubscribeResult> subscribeBulk(int serverHandle, List<String> items) {
            List<SubscribeResult> results = new ArrayList<>();
            for (int index = 0; index < items.size(); index++) {
                results.add(SubscribeResult.newBuilder()
                        .setServerHandle(serverHandle)
                        .setTagAddress(items.get(index))
                        .setItemHandle(100 + index)
                        .setWasSuccessful(true)
                        .build());
            }
            return results;
        }

        @Override
        public List<SubscribeResult> unsubscribeBulk(int serverHandle, List<Integer> itemHandles) {
            List<SubscribeResult> results = new ArrayList<>();
            for (Integer itemHandle : itemHandles) {
                results.add(SubscribeResult.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(itemHandle)
                        .setWasSuccessful(true)
                        .build());
            }
            return results;
        }

        @Override
        public List<BulkReadResult> readBulk(int serverHandle, List<String> items, Duration timeout) {
            List<BulkReadResult> results = new ArrayList<>();
            for (int index = 0; index < items.size(); index++) {
                results.add(BulkReadResult.newBuilder()
                        .setServerHandle(serverHandle)
                        .setTagAddress(items.get(index))
                        .setItemHandle(200 + index)
                        .setWasSuccessful(true)
                        .setWasCached(true)
                        .build());
            }
            return results;
        }

        @Override
        public List<BulkWriteResult> writeBulk(int serverHandle, List<WriteBulkEntry> entries) {
            List<BulkWriteResult> results = new ArrayList<>();
            for (WriteBulkEntry entry : entries) {
                results.add(BulkWriteResult.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(entry.getItemHandle())
                        .setWasSuccessful(true)
                        .build());
            }
            return results;
        }

        @Override
        public List<BulkWriteResult> write2Bulk(int serverHandle, List<Write2BulkEntry> entries) {
            List<BulkWriteResult> results = new ArrayList<>();
            for (Write2BulkEntry entry : entries) {
                results.add(BulkWriteResult.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(entry.getItemHandle())
                        .setWasSuccessful(true)
                        .build());
            }
            return results;
        }

        @Override
        public List<BulkWriteResult> writeSecuredBulk(int serverHandle, List<WriteSecuredBulkEntry> entries) {
            List<BulkWriteResult> results = new ArrayList<>();
            for (WriteSecuredBulkEntry entry : entries) {
                results.add(BulkWriteResult.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(entry.getItemHandle())
                        .setWasSuccessful(true)
                        .build());
            }
            return results;
        }

        @Override
        public List<BulkWriteResult> writeSecured2Bulk(int serverHandle, List<WriteSecured2BulkEntry> entries) {
            List<BulkWriteResult> results = new ArrayList<>();
            for (WriteSecured2BulkEntry entry : entries) {
                results.add(BulkWriteResult.newBuilder()
                        .setServerHandle(serverHandle)
                        .setItemHandle(entry.getItemHandle())
                        .setWasSuccessful(true)
                        .build());
            }
            return results;
        }

        @Override
        public com.zb.mom.ww.mxgateway.client.MxEventStream streamEventsAfter(long afterWorkerSequence) {
            throw new UnsupportedOperationException("stream-events is covered by client tests");
        }
    }

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