package service

import (
	"strings"
	"testing"
	"time"
)

func TestBuildStageHeartbeatProgresses(t *testing.T) {
	p1, m1 := buildStageHeartbeat("titan-13", "titan-04", 10*time.Second)
	p2, m2 := buildStageHeartbeat("titan-13", "titan-04", 3*time.Minute)
	p3, m3 := buildStageHeartbeat("titan-13", "titan-04", 11*time.Minute)

	if !(p1 > 8 && p1 < p2 && p2 < p3 && p3 <= 76) {
		t.Fatalf("unexpected build progress values: %v %v %v", p1, p2, p3)
	}
	if !strings.Contains(m1, "Scheduling") {
		t.Fatalf("expected scheduling message, got %q", m1)
	}
	if !strings.Contains(m2, "replacement image filesystem") {
		t.Fatalf("expected filesystem message, got %q", m2)
	}
	if !strings.Contains(m3, "Harbor") {
		t.Fatalf("expected Harbor message, got %q", m3)
	}
}

func TestFlashStageHeartbeatProgresses(t *testing.T) {
	p1, m1 := flashStageHeartbeat("titan-22", "registry.bstein.dev/metis/titan-13:latest", 5*time.Second)
	p2, m2 := flashStageHeartbeat("titan-22", "registry.bstein.dev/metis/titan-13:latest", 20*time.Second)
	p3, m3 := flashStageHeartbeat("titan-22", "registry.bstein.dev/metis/titan-13:latest", 90*time.Second)

	if !(p1 > 84 && p1 < p2 && p2 < p3 && p3 <= 98) {
		t.Fatalf("unexpected flash progress values: %v %v %v", p1, p2, p3)
	}
	if !strings.Contains(m1, "Pulling") {
		t.Fatalf("expected pulling message, got %q", m1)
	}
	if !strings.Contains(m2, "Writing") {
		t.Fatalf("expected writing message, got %q", m2)
	}
	if !strings.Contains(m3, "Flushing") {
		t.Fatalf("expected flushing message, got %q", m3)
	}
}

func TestParseRemoteProgressLogsFindsLatestMarker(t *testing.T) {
	logs := strings.Join([]string{
		"plain log line",
		ProgressLogLine(RemoteProgressUpdate{Stage: "build", ProgressPct: 40, Message: "phase one"}),
		ProgressLogLine(RemoteProgressUpdate{Stage: "build", ProgressPct: 72, Message: "phase two"}),
		`{"local_path":"/tmp/out.img.xz"}`,
	}, "\n")
	update, ok := parseRemoteProgressLogs(logs)
	if !ok {
		t.Fatal("expected progress marker")
	}
	if update.ProgressPct != 72 || update.Message != "phase two" {
		t.Fatalf("unexpected update: %#v", update)
	}
}

func TestParseRemoteProgressLogsSkipsMalformedMarkers(t *testing.T) {
	logs := strings.Join([]string{
		"plain log line",
		progressLogPrefix + "{bad-json",
		ProgressLogLine(RemoteProgressUpdate{Stage: "flash", ProgressPct: 90, Message: "writing"}),
	}, "\n")
	update, ok := parseRemoteProgressLogs(logs)
	if !ok {
		t.Fatal("expected valid marker after malformed marker")
	}
	if update.Stage != "flash" || update.ProgressPct != 90 || update.Message != "writing" {
		t.Fatalf("unexpected update: %#v", update)
	}
}

func TestApplyRemoteProgressUpdatesOnlyRunningJobs(t *testing.T) {
	app := newTestApp(t)
	running := app.newJob("replace", "titan-13", "titan-22", "/dev/sda")
	app.setJob(running.ID, func(j *Job) {
		j.Status = JobRunning
		j.Stage = "queued"
		j.ProgressPct = 10
	})
	queued := app.newJob("build", "titan-14", "titan-22", "")

	app.applyRemoteProgress("", RemoteProgressUpdate{Stage: "ignored"})
	app.applyRemoteProgress("missing", RemoteProgressUpdate{Stage: "ignored"})
	app.applyRemoteProgress(queued.ID, RemoteProgressUpdate{Stage: "ignored", ProgressPct: 99})
	app.applyRemoteProgress(
		running.ID,
		RemoteProgressUpdate{
			Stage:        "flash",
			ProgressPct:  80,
			Message:      "writing image",
			WrittenBytes: 1024,
			TotalBytes:   2048,
		},
	)
	app.applyRemoteProgress(running.ID, RemoteProgressUpdate{ProgressPct: 70})

	got := app.job(running.ID)
	if got.Stage != "flash" || got.ProgressPct != 80 || got.Message != "writing image" {
		t.Fatalf("running job was not updated correctly: %+v", got)
	}
	if got.Written != 1024 || got.Total != 2048 || got.StageStartedAt.IsZero() {
		t.Fatalf("remote byte/stage fields not applied: %+v", got)
	}
	if app.job(queued.ID).Stage == "ignored" {
		t.Fatalf("queued job should not receive remote progress")
	}
}
