package protocol

import (
	"encoding/json"
	"testing"
	"time"
)

func TestEnvelopeValidate_AgentHello(t *testing.T) {
	payload := AgentHelloPayload{
		NodeID:          "node-01",
		AgentVersion:    "0.1.0",
		ProtocolVersion: CurrentProtocolVersion,
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}

	env := &Envelope{
		ProtocolVersion: CurrentProtocolVersion,
		MessageID:       "msg-001",
		Type:            TypeAgentHello,
		Timestamp:       time.Now().UTC(),
		Payload:         raw,
	}

	if err := env.Validate(); err != nil {
		t.Fatalf("expected valid envelope, got %v", err)
	}
	if !env.Type.IsAgentToControl() {
		t.Fatal("agent.hello should be agent-to-control")
	}
}

func TestEnvelopeValidate_ServerStart(t *testing.T) {
	payload := ServerStartPayload{
		OperationMeta: OperationMeta{
			ServerID:   "srv-01",
			Generation: 4,
		},
		DesiredGeneration: 4,
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}

	env := &Envelope{
		ProtocolVersion: CurrentProtocolVersion,
		MessageID:       "msg-002",
		CorrelationID:   "corr-002",
		Type:            TypeServerStart,
		Timestamp:       time.Now().UTC(),
		Payload:         raw,
	}

	if err := env.Validate(); err != nil {
		t.Fatalf("expected valid envelope, got %v", err)
	}
	if !env.Type.IsControlToAgent() {
		t.Fatal("server.start should be control-to-agent")
	}
}

func TestEnvelopeValidate_RejectsInvalid(t *testing.T) {
	tests := []struct {
		name string
		env  Envelope
	}{
		{
			name: "missing message id",
			env: Envelope{
				ProtocolVersion: 1,
				Type:            TypeAgentHeartbeat,
				Timestamp:       time.Now().UTC(),
				Payload:         json.RawMessage(`{"nodeId":"n1"}`),
			},
		},
		{
			name: "unknown type",
			env: Envelope{
				ProtocolVersion: 1,
				MessageID:       "m1",
				Type:            "agent.unknown",
				Timestamp:       time.Now().UTC(),
				Payload:         json.RawMessage(`{}`),
			},
		},
		{
			name: "non-utc timestamp",
			env: Envelope{
				ProtocolVersion: 1,
				MessageID:       "m1",
				Type:            TypeAgentHeartbeat,
				Timestamp:       time.Now().Local(),
				Payload:         json.RawMessage(`{"nodeId":"n1"}`),
			},
		},
		{
			name: "invalid payload json",
			env: Envelope{
				ProtocolVersion: 1,
				MessageID:       "m1",
				Type:            TypeAgentHeartbeat,
				Timestamp:       time.Now().UTC(),
				Payload:         json.RawMessage(`{not-json`),
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if err := tc.env.Validate(); err == nil {
				t.Fatal("expected validation error")
			}
		})
	}
}

func TestDecodeEnvelope(t *testing.T) {
	data := []byte(`{
		"protocolVersion": 1,
		"messageId": "01HXYZ",
		"type": "agent.heartbeat",
		"timestamp": "2026-01-01T00:00:00Z",
		"payload": {"nodeId":"node-01","runningServers":0}
	}`)

	env, err := DecodeEnvelope(data)
	if err != nil {
		t.Fatalf("decode failed: %v", err)
	}
	if env.Type != TypeAgentHeartbeat {
		t.Fatalf("unexpected type %s", env.Type)
	}
}

func TestServerProvisionPayload_JSONRoundtrip(t *testing.T) {
	deadline := time.Date(2026, 6, 26, 12, 0, 0, 0, time.UTC)
	original := ServerProvisionPayload{
		OperationMeta: OperationMeta{
			ServerID:       "srv-abc123",
			Generation:     2,
			Deadline:       &deadline,
			IdempotencyKey: "idem-001",
		},
		Edition:          "JAVA",
		SoftwareFamily:   "PAPER",
		MinecraftVersion: "1.21.1",
		HostPort:         25565,
		RamMb:            2048,
		DataPath:         "/var/lib/hexahost-gamecloud/srv-abc123",
		EulaAccepted:     true,
	}

	data, err := json.Marshal(original)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}

	var decoded ServerProvisionPayload
	if err := json.Unmarshal(data, &decoded); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}

	if decoded.ServerID != original.ServerID {
		t.Fatalf("serverId: got %q want %q", decoded.ServerID, original.ServerID)
	}
	if decoded.Generation != original.Generation {
		t.Fatalf("generation: got %d want %d", decoded.Generation, original.Generation)
	}
	if decoded.Edition != original.Edition {
		t.Fatalf("edition: got %q want %q", decoded.Edition, original.Edition)
	}
	if decoded.SoftwareFamily != original.SoftwareFamily {
		t.Fatalf("softwareFamily: got %q want %q", decoded.SoftwareFamily, original.SoftwareFamily)
	}
	if decoded.MinecraftVersion != original.MinecraftVersion {
		t.Fatalf("minecraftVersion: got %q want %q", decoded.MinecraftVersion, original.MinecraftVersion)
	}
	if decoded.HostPort != original.HostPort {
		t.Fatalf("hostPort: got %d want %d", decoded.HostPort, original.HostPort)
	}
	if decoded.RamMb != original.RamMb {
		t.Fatalf("ramMb: got %d want %d", decoded.RamMb, original.RamMb)
	}
	if decoded.DataPath != original.DataPath {
		t.Fatalf("dataPath: got %q want %q", decoded.DataPath, original.DataPath)
	}
	if decoded.EulaAccepted != original.EulaAccepted {
		t.Fatalf("eulaAccepted: got %v want %v", decoded.EulaAccepted, original.EulaAccepted)
	}
	if decoded.IdempotencyKey != original.IdempotencyKey {
		t.Fatalf("idempotencyKey: got %q want %q", decoded.IdempotencyKey, original.IdempotencyKey)
	}
	if decoded.Deadline == nil || !decoded.Deadline.Equal(deadline) {
		t.Fatalf("deadline mismatch: got %v want %v", decoded.Deadline, deadline)
	}
}

func TestNewEnvelope(t *testing.T) {
	env, err := NewEnvelope(TypeAgentHello, "msg-new", AgentHelloPayload{
		NodeID:          "n1",
		AgentVersion:    "0.1.0",
		ProtocolVersion: 1,
	})
	if err != nil {
		t.Fatalf("new envelope: %v", err)
	}
	if env.ProtocolVersion != CurrentProtocolVersion {
		t.Fatalf("protocol version %d", env.ProtocolVersion)
	}
}

func TestPhase3Payloads_JSONRoundtrip(t *testing.T) {
	meta := OperationMeta{
		ServerID:   "srv-01",
		Generation: 3,
	}

	command := ServerCommandPayload{
		OperationMeta: meta,
		Command:       "say hello",
	}
	commandRaw, err := json.Marshal(command)
	if err != nil {
		t.Fatal(err)
	}
	var decodedCommand ServerCommandPayload
	if err := json.Unmarshal(commandRaw, &decodedCommand); err != nil {
		t.Fatal(err)
	}
	if decodedCommand.Command != command.Command {
		t.Fatalf("command: got %q want %q", decodedCommand.Command, command.Command)
	}

	logsSub := ServerLogsSubscribePayload{
		OperationMeta: meta,
		TailLines:     100,
		Follow:        true,
	}
	logsSubRaw, err := json.Marshal(logsSub)
	if err != nil {
		t.Fatal(err)
	}
	var decodedLogsSub ServerLogsSubscribePayload
	if err := json.Unmarshal(logsSubRaw, &decodedLogsSub); err != nil {
		t.Fatal(err)
	}
	if decodedLogsSub.TailLines != 100 || !decodedLogsSub.Follow {
		t.Fatalf("logs subscribe mismatch: %+v", decodedLogsSub)
	}

	filesList := ServerFilesListPayload{
		OperationMeta: meta,
		Path:          "world",
	}
	filesListRaw, err := json.Marshal(filesList)
	if err != nil {
		t.Fatal(err)
	}
	env := &Envelope{
		ProtocolVersion: CurrentProtocolVersion,
		MessageID:       "msg-files-list",
		Type:            TypeServerFilesList,
		Timestamp:       time.Now().UTC(),
		Payload:         filesListRaw,
	}
	if err := env.Validate(); err != nil {
		t.Fatalf("files list envelope: %v", err)
	}

	filesRead := ServerFilesReadPayload{
		OperationMeta: meta,
		Path:          "server.properties",
		MaxBytes:      4096,
	}
	filesReadRaw, err := json.Marshal(filesRead)
	if err != nil {
		t.Fatal(err)
	}
	env = &Envelope{
		ProtocolVersion: CurrentProtocolVersion,
		MessageID:       "msg-files-read",
		Type:            TypeServerFilesRead,
		Timestamp:       time.Now().UTC(),
		Payload:         filesReadRaw,
	}
	if err := env.Validate(); err != nil {
		t.Fatalf("files read envelope: %v", err)
	}

	filesWrite := ServerFilesWritePayload{
		OperationMeta: meta,
		Path:          "ops.txt",
		Content:       "test",
		Create:          true,
	}
	filesWriteRaw, err := json.Marshal(filesWrite)
	if err != nil {
		t.Fatal(err)
	}
	env = &Envelope{
		ProtocolVersion: CurrentProtocolVersion,
		MessageID:       "msg-files-write",
		Type:            TypeServerFilesWrite,
		Timestamp:       time.Now().UTC(),
		Payload:         filesWriteRaw,
	}
	if err := env.Validate(); err != nil {
		t.Fatalf("files write envelope: %v", err)
	}

	logLine := ServerLogPayload{
		OperationMeta: meta,
		Line:          "[Server] Done",
		Stream:        "stdout",
	}
	logLineRaw, err := json.Marshal(logLine)
	if err != nil {
		t.Fatal(err)
	}
	env = &Envelope{
		ProtocolVersion: CurrentProtocolVersion,
		MessageID:       "msg-log",
		Type:            TypeServerLog,
		Timestamp:       time.Now().UTC(),
		Payload:         logLineRaw,
	}
	if err := env.Validate(); err != nil {
		t.Fatalf("server log envelope: %v", err)
	}
}

func TestServerOperationResultPayload_WithResult(t *testing.T) {
	listResult, err := json.Marshal(ServerFilesListResultPayload{
		Entries: []FileEntry{{
			Name:       "server.properties",
			Path:       "server.properties",
			IsDir:      false,
			Size:       128,
			ModifiedAt: "2026-06-26T12:00:00Z",
		}},
	})
	if err != nil {
		t.Fatal(err)
	}

	payload := ServerOperationResultPayload{
		OperationMeta: OperationMeta{ServerID: "srv-01", Generation: 1},
		ResultCode:    "ok",
		Operation:     "files.list",
		Result:        listResult,
	}
	raw, err := json.Marshal(payload)
	if err != nil {
		t.Fatal(err)
	}

	var decoded ServerOperationResultPayload
	if err := json.Unmarshal(raw, &decoded); err != nil {
		t.Fatal(err)
	}
	if decoded.Operation != "files.list" {
		t.Fatalf("operation: got %q", decoded.Operation)
	}
	if len(decoded.Result) == 0 {
		t.Fatal("expected result payload")
	}

	var result ServerFilesListResultPayload
	if err := json.Unmarshal(decoded.Result, &result); err != nil {
		t.Fatal(err)
	}
	if len(result.Entries) != 1 || result.Entries[0].Name != "server.properties" {
		t.Fatalf("unexpected list result: %+v", result)
	}
}
