package api

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

func TestEnvelopeRoundTrip(t *testing.T) {
	t.Parallel()

	hello := HelloPayload{
		ProtocolVersion: CurrentProtocolVersion,
		AgentVersion:    "0.1.0",
		ResticVersion:   "0.17.1",
		Hostname:        "test-host",
		OS:              OSLinux,
		Arch:            ArchAmd64,
	}

	env, err := Marshal(MsgHello, "", hello)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}

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

	var decoded Envelope
	if err := json.Unmarshal(wire, &decoded); err != nil {
		t.Fatalf("unmarshal envelope: %v", err)
	}
	if decoded.Type != MsgHello {
		t.Errorf("type: got %q want %q", decoded.Type, MsgHello)
	}

	var got HelloPayload
	if err := decoded.UnmarshalPayload(&got); err != nil {
		t.Fatalf("unmarshal payload: %v", err)
	}
	if got != hello {
		t.Errorf("round-trip mismatch: %+v != %+v", got, hello)
	}
}

func TestEnvelopeNilPayload(t *testing.T) {
	t.Parallel()

	env, err := Marshal(MsgHeartbeat, "", nil)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	if len(env.Payload) != 0 {
		t.Errorf("nil payload should encode as empty, got %q", env.Payload)
	}
	// Unmarshalling nothing into anything must not error.
	var hb HeartbeatPayload
	if err := env.UnmarshalPayload(&hb); err != nil {
		t.Errorf("unmarshal empty payload: %v", err)
	}
}

func TestEnvelopeRPCCorrelation(t *testing.T) {
	t.Parallel()

	cmd := CommandRunPayload{JobID: "01HJ8K7", Kind: JobBackup}
	env, err := Marshal(MsgCommandRun, "req-1", cmd)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	if env.ID != "req-1" {
		t.Errorf("id not preserved: %q", env.ID)
	}

	res := CommandResultPayload{JobID: "01HJ8K7", Accepted: true}
	resEnv, err := Marshal(MsgCommandResult, env.ID, res)
	if err != nil {
		t.Fatalf("marshal result: %v", err)
	}
	if resEnv.ID != env.ID {
		t.Errorf("rpc id mismatch: req=%q res=%q", env.ID, resEnv.ID)
	}
}

func TestErrorPayload(t *testing.T) {
	t.Parallel()

	ep := ErrorPayload{
		Code:    ErrProtocolTooOld,
		Message: "agent protocol_version 0 below minimum 1",
		HelpURL: "https://example.com/upgrade",
	}
	env, err := Marshal(MsgError, "", ep)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	wire, _ := json.Marshal(env)

	var decoded Envelope
	if err := json.Unmarshal(wire, &decoded); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	var got ErrorPayload
	if err := decoded.UnmarshalPayload(&got); err != nil {
		t.Fatalf("unmarshal payload: %v", err)
	}
	if got.Code != ErrProtocolTooOld {
		t.Errorf("code: got %q want %q", got.Code, ErrProtocolTooOld)
	}
}

func TestProtocolVersionConstants(t *testing.T) {
	t.Parallel()

	if CurrentProtocolVersion < 1 {
		t.Errorf("CurrentProtocolVersion must be >= 1, got %d", CurrentProtocolVersion)
	}
	if MinAgentProtocolVersion > CurrentProtocolVersion {
		t.Errorf("min %d > current %d — server would refuse all agents",
			MinAgentProtocolVersion, CurrentProtocolVersion)
	}
}

func TestJobProgressShapeStable(t *testing.T) {
	t.Parallel()
	// Locks the JSON field names from spec.md §6.2 so a rename here
	// breaks tests instead of silently breaking the agent.
	p := JobProgressPayload{
		JobID: "j", PercentDone: 0.5, FilesDone: 1, TotalFiles: 2,
		BytesDone: 100, TotalBytes: 200, ETASeconds: 30, ThroughputBps: 1000,
	}
	raw, _ := json.Marshal(p)
	want := `{"job_id":"j","percent_done":0.5,"files_done":1,"total_files":2,"bytes_done":100,"total_bytes":200,"eta_seconds":30,"throughput_bps":1000}`
	if string(raw) != want {
		t.Errorf("wire shape drifted:\n  got  %s\n  want %s", raw, want)
	}
}

func TestRepoStatsPayloadRoundTrip(t *testing.T) {
	t.Parallel()

	// Nil pointer fields must be omitted from JSON output.
	empty := RepoStatsPayload{}
	raw, err := json.Marshal(empty)
	if err != nil {
		t.Fatalf("marshal empty: %v", err)
	}
	if string(raw) != "{}" {
		t.Errorf("empty payload should marshal to {}, got %s", raw)
	}

	// Populated fields must survive a round trip.
	total := int64(123456)
	rawSize := int64(200000)
	files := int64(42)
	snaps := int64(7)
	lockPresent := true
	now := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
	pruneAt := time.Date(2026, 1, 3, 0, 0, 0, 0, time.UTC)
	freed := int64(8192)

	p := RepoStatsPayload{
		TotalSizeBytes:      &total,
		RawSizeBytes:        &rawSize,
		UniqueFiles:         &files,
		SnapshotCount:       &snaps,
		LastCheckAt:         &now,
		LastCheckStatus:     "ok",
		LockPresent:         &lockPresent,
		LastPruneAt:         &pruneAt,
		LastPruneFreedBytes: &freed,
	}
	raw2, err := json.Marshal(p)
	if err != nil {
		t.Fatalf("marshal full: %v", err)
	}
	var got RepoStatsPayload
	if err := json.Unmarshal(raw2, &got); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if got.TotalSizeBytes == nil || *got.TotalSizeBytes != total {
		t.Errorf("TotalSizeBytes: got %v, want %d", got.TotalSizeBytes, total)
	}
	if got.RawSizeBytes == nil || *got.RawSizeBytes != rawSize {
		t.Errorf("RawSizeBytes: got %v, want %d", got.RawSizeBytes, rawSize)
	}
	if got.UniqueFiles == nil || *got.UniqueFiles != files {
		t.Errorf("UniqueFiles: got %v, want %d", got.UniqueFiles, files)
	}
	if got.SnapshotCount == nil || *got.SnapshotCount != snaps {
		t.Errorf("SnapshotCount: got %v, want %d", got.SnapshotCount, snaps)
	}
	if got.LastCheckAt == nil || !got.LastCheckAt.Equal(now) {
		t.Errorf("LastCheckAt: got %v, want %v", got.LastCheckAt, now)
	}
	if got.LastCheckStatus != "ok" {
		t.Errorf("LastCheckStatus: got %q, want %q", got.LastCheckStatus, "ok")
	}
	if got.LockPresent == nil || *got.LockPresent != lockPresent {
		t.Errorf("LockPresent: got %v, want %v", got.LockPresent, lockPresent)
	}
	if got.LastPruneAt == nil || !got.LastPruneAt.Equal(pruneAt) {
		t.Errorf("LastPruneAt: got %v, want %v", got.LastPruneAt, pruneAt)
	}
	if got.LastPruneFreedBytes == nil || *got.LastPruneFreedBytes != freed {
		t.Errorf("LastPruneFreedBytes: got %v, want %d", got.LastPruneFreedBytes, freed)
	}

	// Partial update: only set LockPresent.
	lockFalse := false
	partial := RepoStatsPayload{LockPresent: &lockFalse}
	rawPartial, _ := json.Marshal(partial)
	if string(rawPartial) != `{"lock_present":false}` {
		t.Errorf("partial marshal: got %s, want {\"lock_present\":false}", rawPartial)
	}
}

// touch time so the import is used by other tests in this file when
// they grow over time.
var _ = time.Now
