package deployer

import (
	"context"
	"errors"
	"sync/atomic"
	"testing"

	"github.com/alexei/tinyforge/internal/store"
	"github.com/alexei/tinyforge/internal/workload/plugin"
)

// fakeBackuper records pre-deploy backup calls so the dispatch wiring can be
// asserted. err (when set) simulates a backup failure.
type fakeBackuper struct {
	count    atomic.Int32
	lastType atomic.Value // string
	err      error
}

func (f *fakeBackuper) CreateBackup(backupType string) (store.Backup, error) {
	f.count.Add(1)
	f.lastType.Store(backupType)
	if f.err != nil {
		return store.Backup{}, f.err
	}
	return store.Backup{ID: "b1", Filename: "tinyforge-pre-deploy.db"}, nil
}

func setAutoBackup(t *testing.T, d *Deployer, enabled bool) {
	t.Helper()
	s, err := d.store.GetSettings()
	if err != nil {
		t.Fatalf("get settings: %v", err)
	}
	s.AutoBackupBeforeDeploy = enabled
	if err := d.store.UpdateSettings(s); err != nil {
		t.Fatalf("update settings: %v", err)
	}
}

// Regression: the pre-deploy backup hook was orphaned after the cutover (no
// caller on DispatchPlugin), making auto_backup_before_deploy a silent no-op.
func TestDispatchPlugin_PreDeployBackup_FiresWhenEnabled(t *testing.T) {
	resetFake(t)
	d := newTestDeployer(t)
	b := &fakeBackuper{}
	d.SetPreDeployBackuper(b)
	setAutoBackup(t, d, true)

	if err := d.DispatchPlugin(context.Background(), sampleWorkload(), plugin.DeploymentIntent{}); err != nil {
		t.Fatalf("dispatch: %v", err)
	}
	if got := b.count.Load(); got != 1 {
		t.Fatalf("CreateBackup called %d times, want 1", got)
	}
	if bt, _ := b.lastType.Load().(string); bt != "pre-deploy" {
		t.Fatalf("backup type = %q, want pre-deploy", bt)
	}
	if got := dispatchTestSource.deployCount.Load(); got != 1 {
		t.Fatalf("Deploy ran %d times, want 1", got)
	}
}

func TestDispatchPlugin_PreDeployBackup_SkippedWhenDisabled(t *testing.T) {
	resetFake(t)
	d := newTestDeployer(t)
	b := &fakeBackuper{}
	d.SetPreDeployBackuper(b)
	setAutoBackup(t, d, false)

	if err := d.DispatchPlugin(context.Background(), sampleWorkload(), plugin.DeploymentIntent{}); err != nil {
		t.Fatalf("dispatch: %v", err)
	}
	if got := b.count.Load(); got != 0 {
		t.Fatalf("CreateBackup called %d times, want 0 (setting off)", got)
	}
}

func TestDispatchPlugin_PreDeployBackup_NilBackuperNoPanic(t *testing.T) {
	resetFake(t)
	d := newTestDeployer(t)
	setAutoBackup(t, d, true) // enabled, but no backuper wired

	if err := d.DispatchPlugin(context.Background(), sampleWorkload(), plugin.DeploymentIntent{}); err != nil {
		t.Fatalf("dispatch must not panic/fail with a nil backuper: %v", err)
	}
	if got := dispatchTestSource.deployCount.Load(); got != 1 {
		t.Fatalf("Deploy ran %d times, want 1", got)
	}
}

func TestDispatchPlugin_PreDeployBackup_FailOpen(t *testing.T) {
	resetFake(t)
	d := newTestDeployer(t)
	b := &fakeBackuper{err: errors.New("disk full")}
	d.SetPreDeployBackuper(b)
	setAutoBackup(t, d, true)

	// A failed backup is logged but must NOT block the deploy.
	if err := d.DispatchPlugin(context.Background(), sampleWorkload(), plugin.DeploymentIntent{}); err != nil {
		t.Fatalf("deploy must succeed when backup fails (fail-open): %v", err)
	}
	if got := dispatchTestSource.deployCount.Load(); got != 1 {
		t.Fatalf("Deploy ran %d times, want 1 (despite backup failure)", got)
	}
}
