package selfupdate

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"testing"
	"time"
)

// writePending drops a pending.json fixture into the staging dir.
func writePending(t *testing.T, stateDir string, m PendingMarker) {
	t.Helper()
	dir := stagingDir(stateDir)
	if err := os.MkdirAll(dir, 0o750); err != nil {
		t.Fatal(err)
	}
	b, _ := json.Marshal(m)
	if err := os.WriteFile(filepath.Join(dir, "pending.json"), b, 0o644); err != nil {
		t.Fatal(err)
	}
}

func newMgr(t *testing.T, stateDir, runningVersion string, wrap WrapperRunner, buf *bytes.Buffer) *Manager {
	t.Helper()
	var w io.Writer = io.Discard
	if buf != nil {
		w = buf
	}
	m := NewManager(ManagerConfig{
		StateDir:       stateDir,
		RunningVersion: runningVersion,
		Dwell:          time.Hour, // never elapses in the test; we drive it via the sleep seam
		Runner:         wrap,
		Logger:         slog.New(slog.NewTextHandler(w, nil)),
	})
	// Replace the real dwell sleep with an instant one so the test doesn't block.
	m.sleep = func(context.Context, time.Duration) {}
	return m
}

// Scenario D: pending marker names THIS version → after the dwell, `commit` is called.
func TestManager_CommitsMatchingVersion(t *testing.T) {
	stateDir := t.TempDir()
	writePending(t, stateDir, PendingMarker{OldVersion: "0.70.0", NewVersion: "0.70.1", SHA256: "ab", AppliedAt: "t"})
	wrap := &fakeWrapper{}
	m := newMgr(t, stateDir, "0.70.1", wrap, nil)

	m.MaybeCommit(context.Background())

	if len(wrap.calls) != 1 || wrap.calls[0][1] != "commit" {
		t.Fatalf("expected exactly one `commit` call, got %v", wrap.calls)
	}
}

// Scenario C5: pending marker version != running version → commit is NEVER called; a loud WARN is
// logged; the marker is LEFT (so the report keeps showing pending=true).
func TestManager_VersionMismatchDoesNotCommit(t *testing.T) {
	stateDir := t.TempDir()
	writePending(t, stateDir, PendingMarker{OldVersion: "0.70.0", NewVersion: "0.70.5", SHA256: "ab", AppliedAt: "t"})
	wrap := &fakeWrapper{}
	buf := &bytes.Buffer{}
	m := newMgr(t, stateDir, "0.70.1", wrap, buf) // running 0.70.1, marker says 0.70.5

	m.MaybeCommit(context.Background())

	if len(wrap.calls) != 0 {
		t.Errorf("commit called despite version mismatch: %v", wrap.calls)
	}
	if !bytes.Contains(buf.Bytes(), []byte("NOT committing")) {
		t.Errorf("expected a loud WARN; logs:\n%s", buf.String())
	}
	// Marker must remain so the report still flags pending.
	if p, _ := readPending(stateDir); p == nil {
		t.Error("pending marker was removed on a version mismatch (report would lose visibility)")
	}
	// And the report seam reflects it.
	if pending, ver := m.SelfUpdatePending(); !pending || ver != "0.70.5" {
		t.Errorf("SelfUpdatePending() = %v/%q, want true/0.70.5", pending, ver)
	}
}

// No pending marker → nothing happens (the common steady state).
func TestManager_NoPendingNoOp(t *testing.T) {
	stateDir := t.TempDir()
	wrap := &fakeWrapper{}
	m := newMgr(t, stateDir, "0.70.1", wrap, nil)
	m.MaybeCommit(context.Background())
	if len(wrap.calls) != 0 {
		t.Errorf("commit/anything called with no pending marker: %v", wrap.calls)
	}
	if pending, _ := m.SelfUpdatePending(); pending {
		t.Error("SelfUpdatePending() true with no marker")
	}
}

// Shutdown before the dwell elapses → no commit, marker left for the next start.
func TestManager_ShutdownBeforeDwellLeavesPending(t *testing.T) {
	stateDir := t.TempDir()
	writePending(t, stateDir, PendingMarker{NewVersion: "0.70.1"})
	wrap := &fakeWrapper{}
	m := newMgr(t, stateDir, "0.70.1", wrap, nil)
	// A cancelled ctx before the (seam) sleep → MaybeCommit sees ctx.Err() and bails.
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	m.MaybeCommit(ctx)
	if len(wrap.calls) != 0 {
		t.Errorf("commit called during shutdown: %v", wrap.calls)
	}
	if p, _ := readPending(stateDir); p == nil {
		t.Error("pending marker removed on shutdown-before-commit")
	}
}
