package selfupdate

import (
	"context"
	"fmt"
	"io"
	"log"
	"sync"
	"testing"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
	"gitea.dooplex.hu/admin/felhom-controller/internal/config"
)

const imageBase = "gitea.dooplex.hu/admin/felhom-controller"

type fakeAgent struct {
	mu    sync.Mutex
	calls []string
	err   error
}

func (f *fakeAgent) SwapController(_ context.Context, image string) (agentapi.SwapResult, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.calls = append(f.calls, image)
	return agentapi.SwapResult{Status: "swapping", PreviousImage: imageBase + ":0.77.0"}, f.err
}
func (f *fakeAgent) swapCalls() []string {
	f.mu.Lock()
	defer f.mu.Unlock()
	return append([]string(nil), f.calls...)
}

func newTestUpdater(t *testing.T, current string, agent AgentSwapper) *Updater {
	t.Helper()
	cfg := &config.SelfUpdateConfig{Enabled: true, Image: imageBase}
	git := &config.GitConfig{Username: "u", Token: "tok"}
	return NewUpdater(cfg, git, current, t.TempDir(), agent, log.New(io.Discard, "", 0), false)
}

// waitDone polls until an in-flight update finishes (performUpdate runs async).
func waitDone(t *testing.T, u *Updater) {
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for u.IsUpdateRunning() {
		if time.Now().After(deadline) {
			t.Fatal("update did not finish in time")
		}
		time.Sleep(2 * time.Millisecond)
	}
}

// Scenario C — already latest: no pull, no agent call.
func TestTriggerUpdate_UpToDate_NoPullNoAgent(t *testing.T) {
	agent := &fakeAgent{}
	u := newTestUpdater(t, "0.84.0", agent)
	u.queryFn = func() (string, error) { return "0.84.0", nil }
	pulled := false
	u.pullFn = func(string) error { pulled = true; return nil }

	err := u.TriggerUpdate("test")
	if err == nil {
		t.Fatal("expected 'no update' error when already latest")
	}
	if pulled {
		t.Error("must NOT pull when up-to-date")
	}
	if n := len(agent.swapCalls()); n != 0 {
		t.Errorf("agent called %d times, want 0 (up-to-date)", n)
	}
}

// Scenario D — pull fails: agent never called, current controller untouched.
func TestPerformUpdate_PullFails_AgentNotCalled(t *testing.T) {
	agent := &fakeAgent{}
	u := newTestUpdater(t, "0.77.0", agent)
	u.queryFn = func() (string, error) { return "0.84.0", nil }
	u.pullFn = func(string) error { return fmt.Errorf("registry unreachable") }

	if err := u.TriggerUpdate("test"); err != nil {
		t.Fatalf("TriggerUpdate returned %v, want nil (async)", err)
	}
	waitDone(t, u)

	if n := len(agent.swapCalls()); n != 0 {
		t.Errorf("agent called %d times, want 0 (pull failed)", n)
	}
	st, _ := LoadState(u.dataDir)
	if st == nil || st.Status != "failed" {
		t.Errorf("state = %+v, want failed", st)
	}
}

// Happy path — pull then delegate the swap to the agent with the right ref.
func TestPerformUpdate_Happy_PullThenSwap(t *testing.T) {
	agent := &fakeAgent{}
	u := newTestUpdater(t, "0.77.0", agent)
	u.queryFn = func() (string, error) { return "0.84.0", nil }
	var pulledImage string
	u.pullFn = func(img string) error { pulledImage = img; return nil }

	if err := u.TriggerUpdate("test"); err != nil {
		t.Fatalf("TriggerUpdate returned %v, want nil", err)
	}
	waitDone(t, u)

	want := imageBase + ":0.84.0"
	if pulledImage != want {
		t.Errorf("pulled %q, want %q", pulledImage, want)
	}
	calls := agent.swapCalls()
	if len(calls) != 1 || calls[0] != want {
		t.Errorf("agent swap calls = %v, want exactly [%q]", calls, want)
	}
}

// No agent (un-provisioned guest) → update unavailable, never pulls.
func TestTriggerUpdate_NoAgent_Unavailable(t *testing.T) {
	u := newTestUpdater(t, "0.77.0", nil)
	u.queryFn = func() (string, error) { return "0.84.0", nil }
	pulled := false
	u.pullFn = func(string) error { pulled = true; return nil }

	if err := u.TriggerUpdate("test"); err == nil {
		t.Fatal("expected error when no agent is wired")
	}
	if pulled {
		t.Error("must not pull when no agent")
	}
}
