// Verifies the orchestration order in `GalaxyClient.executeCommand`:
//
//   core.signRequest(fields)
//     → signer(canonicalBytes)
//       → edge.executeCommand({ envelope, signature })
//         → core.verifyPayloadHash(response payload, hash)
//           → core.verifyResponse(envelope, signature)
//             → return response.payloadBytes
//
// Tests use a hand-rolled mock `Core` and a Connect router transport
// from `@connectrpc/connect`, which lets us assert what the gateway
// would have received without standing up a real Connect server.

import { create } from "@bufbuild/protobuf";
import { createClient, createRouterTransport } from "@connectrpc/connect";
import { describe, expect, test, vi } from "vitest";
import { GalaxyClient } from "../src/api/galaxy-client";
import {
	EdgeGateway,
	ExecuteCommandResponseSchema,
	type ExecuteCommandRequest,
} from "../src/proto/galaxy/gateway/v1/edge_gateway_pb";
import type {
	Core,
	RequestSigningFields,
	ResponseSigningFields,
} from "../src/platform/core/index";

const FIXED_REQUEST_ID = "req-test-1";
const FIXED_TIMESTAMP = 1_700_000_000_000n;

describe("GalaxyClient.executeCommand", () => {
	test("orchestrates sign → fetch → verify and returns the payload", async () => {
		const canonicalBytes = new Uint8Array([0xca, 0xfe]);
		const signature = new Uint8Array(64).fill(0x55);
		const responsePayload = new TextEncoder().encode("hello-from-server");
		const responseHash = new Uint8Array(32).fill(0x77);
		const responseSignature = new Uint8Array(64).fill(0x99);
		const responsePublicKey = new Uint8Array(32).fill(0x11);

		const core = mockCore({
			signRequestImpl: () => canonicalBytes,
			verifyResponseImpl: () => true,
			verifyPayloadHashImpl: () => true,
		});
		const signer = vi.fn(async () => signature);
		const sha256 = vi.fn(async () => new Uint8Array(32).fill(0x33));
		let captured: ExecuteCommandRequest | undefined;
		const transport = createRouterTransport(({ service }) => {
			service(EdgeGateway, {
				executeCommand(req) {
					captured = req;
					return create(ExecuteCommandResponseSchema, {
						protocolVersion: "v1",
						requestId: FIXED_REQUEST_ID,
						timestampMs: FIXED_TIMESTAMP,
						resultCode: "ok",
						payloadBytes: responsePayload,
						payloadHash: responseHash,
						signature: responseSignature,
					});
				},
				subscribeEvents() {
					throw new Error("not used in this test");
				},
			});
		});
		const edge = createClient(EdgeGateway, transport);

		const client = new GalaxyClient({
			core,
			edge,
			signer,
			sha256,
			deviceSessionId: "device-session-1",
			gatewayResponsePublicKey: responsePublicKey,
			clock: () => FIXED_TIMESTAMP,
			requestIdFactory: () => FIXED_REQUEST_ID,
		});

		const out = await client.executeCommand(
			"user.account.get",
			new TextEncoder().encode("client-payload"),
		);

		expect(out.resultCode).toBe("ok");
		expect(Array.from(out.payloadBytes)).toEqual(Array.from(responsePayload));
		expect(signer).toHaveBeenCalledWith(canonicalBytes);
		expect(sha256).toHaveBeenCalledTimes(1);
		expect(core.signRequest).toHaveBeenCalledTimes(1);
		const verifyHashCall = vi.mocked(core.verifyPayloadHash).mock.calls[0]!;
		expect(Array.from(verifyHashCall[0])).toEqual(Array.from(responsePayload));
		expect(Array.from(verifyHashCall[1])).toEqual(Array.from(responseHash));
		const verifyRespCall = vi.mocked(core.verifyResponse).mock.calls[0]!;
		expect(Array.from(verifyRespCall[0])).toEqual(Array.from(responsePublicKey));
		expect(Array.from(verifyRespCall[1])).toEqual(Array.from(responseSignature));
		expect(verifyRespCall[2]).toMatchObject({
			protocolVersion: "v1",
			requestId: FIXED_REQUEST_ID,
			resultCode: "ok",
		});
		expect(captured?.deviceSessionId).toBe("device-session-1");
		expect(captured?.messageType).toBe("user.account.get");
		expect(captured?.requestId).toBe(FIXED_REQUEST_ID);
		expect(Array.from(captured?.signature ?? [])).toEqual(
			Array.from(signature),
		);
	});

	test("throws when the response signature fails verification", async () => {
		const core = mockCore({
			signRequestImpl: () => new Uint8Array(),
			verifyResponseImpl: () => false,
			verifyPayloadHashImpl: () => true,
		});
		const transport = createRouterTransport(({ service }) => {
			service(EdgeGateway, {
				executeCommand: () =>
					create(ExecuteCommandResponseSchema, {
						protocolVersion: "v1",
						requestId: FIXED_REQUEST_ID,
						timestampMs: FIXED_TIMESTAMP,
						resultCode: "ok",
						payloadBytes: new Uint8Array(),
						payloadHash: new Uint8Array(32),
						signature: new Uint8Array(64),
					}),
				subscribeEvents() {
					throw new Error("not used in this test");
				},
			});
		});
		const client = new GalaxyClient({
			core,
			edge: createClient(EdgeGateway, transport),
			signer: async () => new Uint8Array(64),
			sha256: async () => new Uint8Array(32),
			deviceSessionId: "device-session-1",
			gatewayResponsePublicKey: new Uint8Array(32),
			clock: () => FIXED_TIMESTAMP,
			requestIdFactory: () => FIXED_REQUEST_ID,
		});
		await expect(
			client.executeCommand("user.account.get", new Uint8Array()),
		).rejects.toThrow(/invalid response signature/);
	});

	test("throws when the response payload_hash does not match", async () => {
		const core = mockCore({
			signRequestImpl: () => new Uint8Array(),
			verifyResponseImpl: () => true,
			verifyPayloadHashImpl: () => false,
		});
		const transport = createRouterTransport(({ service }) => {
			service(EdgeGateway, {
				executeCommand: () =>
					create(ExecuteCommandResponseSchema, {
						protocolVersion: "v1",
						requestId: FIXED_REQUEST_ID,
						timestampMs: FIXED_TIMESTAMP,
						resultCode: "ok",
						payloadBytes: new Uint8Array(),
						payloadHash: new Uint8Array(32),
						signature: new Uint8Array(64),
					}),
				subscribeEvents() {
					throw new Error("not used in this test");
				},
			});
		});
		const client = new GalaxyClient({
			core,
			edge: createClient(EdgeGateway, transport),
			signer: async () => new Uint8Array(64),
			sha256: async () => new Uint8Array(32),
			deviceSessionId: "device-session-1",
			gatewayResponsePublicKey: new Uint8Array(32),
			clock: () => FIXED_TIMESTAMP,
			requestIdFactory: () => FIXED_REQUEST_ID,
		});
		await expect(
			client.executeCommand("user.account.get", new Uint8Array()),
		).rejects.toThrow(/payload_hash mismatch/);
	});
});

interface MockCoreOptions {
	signRequestImpl: (fields: RequestSigningFields) => Uint8Array;
	verifyResponseImpl: (
		publicKey: Uint8Array,
		signature: Uint8Array,
		fields: ResponseSigningFields,
	) => boolean;
	verifyPayloadHashImpl: (
		payload: Uint8Array,
		hash: Uint8Array,
	) => boolean;
}

function mockCore(opts: MockCoreOptions): Core & {
	signRequest: ReturnType<typeof vi.fn>;
	verifyResponse: ReturnType<typeof vi.fn>;
	verifyPayloadHash: ReturnType<typeof vi.fn>;
	verifyEvent: ReturnType<typeof vi.fn>;
} {
	return {
		signRequest: vi.fn(opts.signRequestImpl),
		verifyResponse: vi.fn(opts.verifyResponseImpl),
		verifyEvent: vi.fn(() => true),
		verifyPayloadHash: vi.fn(opts.verifyPayloadHashImpl),
		// `GalaxyClient` does not exercise the Phase 18 calc bridge,
		// so these stubs only need to satisfy the `Core` interface.
		driveEffective: () => 0,
		emptyMass: () => 0,
		weaponsBlockMass: () => 0,
		fullMass: () => 0,
		speed: () => 0,
		cargoCapacity: () => 0,
		carryingMass: () => 0,
		blockUpgradeCost: () => 0,
	};
}
