package selfheal

import (
	"context"
	"io"
	"log/slog"
	"testing"
	"time"
)

func quietLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError + 1}))
}

// fakeHeal is a controllable Heal: healthy toggles, and it counts remediation invocations.
type fakeHeal struct {
	healthy    bool
	remediated int
}

func (f *fakeHeal) Name() string { return "fake" }
func (f *fakeHeal) Healthy(context.Context) (bool, string) {
	return f.healthy, "fake detail"
}
func (f *fakeHeal) Remediate(context.Context) error {
	f.remediated++
	f.healthy = true
	return nil
}

// THE BYO GATE (red-proof): a byo Manager must NEVER reach a remedy, even with an unhealthy heal.
func TestManager_ByoNeverRemediates(t *testing.T) {
	h := &fakeHeal{healthy: false}
	m := &Manager{Appliance: false, Heals: []Heal{h}, Logger: quietLogger()}
	m.RunOnce(context.Background())
	if h.remediated != 0 {
		t.Fatalf("byo host must NEVER remediate, got %d invocations", h.remediated)
	}
}

// An appliance Manager remediates an unhealthy heal.
func TestManager_ApplianceRemediates(t *testing.T) {
	h := &fakeHeal{healthy: false}
	m := &Manager{Appliance: true, Heals: []Heal{h}, Logger: quietLogger()}
	m.RunOnce(context.Background())
	if h.remediated != 1 {
		t.Fatalf("appliance must remediate an unhealthy heal once, got %d", h.remediated)
	}
}

// A healthy heal is never remediated (either mode).
func TestManager_HealthyNoRemediate(t *testing.T) {
	for _, appliance := range []bool{true, false} {
		h := &fakeHeal{healthy: true}
		m := &Manager{Appliance: appliance, Heals: []Heal{h}, Logger: quietLogger()}
		m.RunOnce(context.Background())
		if h.remediated != 0 {
			t.Fatalf("appliance=%t: a healthy heal must not be remediated, got %d", appliance, h.remediated)
		}
	}
}

// THE BYO GATE at the networking layer: force the networking check unhealthy under byo and assert the
// privileged Start is invoked ZERO times (the gate is before any exec).
func TestNetworkingHeal_ByoZeroPrivilegedInvocations(t *testing.T) {
	starts := 0
	n := &NetworkingHeal{
		IsActive: func(context.Context) bool { return false },
		HasRoute: func(context.Context) bool { return false },
		Start:    func(context.Context) error { starts++; return nil },
		Sleep:    func(context.Context, time.Duration) {},
		Logger:   quietLogger(),
	}
	m := &Manager{Appliance: false, Heals: []Heal{n}, Logger: quietLogger()}
	m.RunOnce(context.Background())
	if starts != 0 {
		t.Fatalf("byo networking heal must invoke `systemctl start` ZERO times, got %d", starts)
	}
}

// The remedy state machine: recovers on the 2nd attempt (start brings it up) → returns nil, one INFO.
func TestNetworkingHeal_RecoversMidAttempts(t *testing.T) {
	defer withFastBackoffs()()
	active := false
	attempts := 0
	n := &NetworkingHeal{
		IsActive: func(context.Context) bool { return active },
		HasRoute: func(context.Context) bool { return true },
		Start: func(context.Context) error {
			attempts++
			if attempts >= 2 {
				active = true // the 2nd start brings networking up
			}
			return nil
		},
		Sleep:  func(context.Context, time.Duration) {},
		Logger: quietLogger(),
	}
	if err := n.Remediate(context.Background()); err != nil {
		t.Fatalf("remedy should recover by attempt 2, got err %v", err)
	}
	if attempts != 2 {
		t.Fatalf("recovery should have taken 2 start attempts, got %d", attempts)
	}
}

// The remedy gives up after all attempts if networking never comes up — a terminal error, logged as
// give-up by the Manager.
func TestNetworkingHeal_GivesUpAfterAllAttempts(t *testing.T) {
	defer withFastBackoffs()()
	starts := 0
	n := &NetworkingHeal{
		IsActive: func(context.Context) bool { return false }, // never recovers
		HasRoute: func(context.Context) bool { return false },
		Start:    func(context.Context) error { starts++; return nil },
		Sleep:    func(context.Context, time.Duration) {},
		Logger:   quietLogger(),
	}
	err := n.Remediate(context.Background())
	if err == nil {
		t.Fatal("remedy must return a terminal error when networking never recovers")
	}
	if starts != len(networkingBackoffs) {
		t.Fatalf("remedy must try exactly %d times, got %d", len(networkingBackoffs), starts)
	}
}

// withFastBackoffs swaps the real 10/30/60s backoffs for near-zero waits (the Sleep seam is faked
// anyway, but keep the count/shape). Returns a restore func.
func withFastBackoffs() func() {
	orig := networkingBackoffs
	networkingBackoffs = []time.Duration{time.Millisecond, time.Millisecond, time.Millisecond}
	return func() { networkingBackoffs = orig }
}
