package backup

import (
	"io"
	"log"
	"path/filepath"
	"testing"
)

// TestRestoreFromRecoveryUnitOrchestration exercises the full in-process flow: read manifest →
// recover secrets → apply gate → recreate with the reconciled env. It proves (a) on success the
// recreate is called with non-secret env + recovered secrets merged, and (b) on a missing data-key the
// restore is REFUSED and recreate is never called.
func TestRestoreFromRecoveryUnitOrchestration(t *testing.T) {
	newUnit := func(t *testing.T) (drive string) {
		tmp := t.TempDir()
		drive = filepath.Join(tmp, "drive")
		// stripped (secret-free) app.yaml in the unit
		mustWrite(t, filepath.Join(RecoveryUnitComposePath(drive, "app"), "app.yaml"),
			"deployed: true\nenv:\n  SUBDOMAIN: trips\n")
		man := &RecoveryManifest{SchemaVersion: 1, AppName: "app", ControllerVer: "v",
			SecretEnvVars: []string{"DB_PASSWORD", "SECRET_KEY"}, DataKeyEnvVars: []string{"SECRET_KEY"}}
		if err := writeManifest(RecoveryUnitManifestPath(drive, "app"), man); err != nil {
			t.Fatal(err)
		}
		return drive
	}

	t.Run("success — recreate called with merged env", func(t *testing.T) {
		drive := newUnit(t)
		fake := &fakeRecoveryProvider{
			hdd:     drive,
			running: true, // so the post-restore health wait returns promptly
			secrets: map[string]string{"DB_PASSWORD": "pw", "SECRET_KEY": "deadbeef"},
		}
		m := &Manager{logger: log.New(io.Discard, "", 0), systemDataPath: filepath.Join(drive, "..", "sys"),
			stackProvider: fake}
		if err := m.RestoreFromRecoveryUnit("app"); err != nil {
			t.Fatalf("restore: %v", err)
		}
		if fake.gotEnv == nil {
			t.Fatal("recreate was not called")
		}
		if fake.gotEnv["SUBDOMAIN"] != "trips" || fake.gotEnv["DB_PASSWORD"] != "pw" || fake.gotEnv["SECRET_KEY"] != "deadbeef" {
			t.Errorf("recreate got wrong env: %v", fake.gotEnv)
		}
		if !fake.stopped {
			t.Errorf("app should be stopped before restore")
		}
	})

	t.Run("data-key unrecoverable — REFUSED, recreate not called", func(t *testing.T) {
		drive := newUnit(t)
		fake := &fakeRecoveryProvider{
			hdd:     drive,
			secrets: map[string]string{"DB_PASSWORD": "pw"}, // SECRET_KEY (data_key) missing
		}
		m := &Manager{logger: log.New(io.Discard, "", 0), systemDataPath: filepath.Join(drive, "..", "sys"),
			stackProvider: fake}
		err := m.RestoreFromRecoveryUnit("app")
		if err == nil {
			t.Fatal("expected fail-closed refusal, got nil")
		}
		if fake.gotEnv != nil {
			t.Errorf("recreate must NOT be called on refusal, got %v", fake.gotEnv)
		}
	})
}

// TestReconcileRestoreSecrets covers the safety-critical fail-closed gate + secret reconciliation.
func TestReconcileRestoreSecrets(t *testing.T) {
	nonSecret := map[string]string{"SUBDOMAIN": "trips", "DOMAIN": "demo-felhom.eu"}

	t.Run("all recovered, no data_key — full env, no error", func(t *testing.T) {
		recovered := map[string]string{"DB_PASSWORD": "pw", "SECRET_KEY": "deadbeef"}
		full, missing, err := reconcileRestoreSecrets(nonSecret, recovered,
			[]string{"DB_PASSWORD", "SECRET_KEY"}, nil)
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if len(missing) != 0 {
			t.Errorf("missing: %v", missing)
		}
		// Non-secret + both secrets present, and recovered values used VERBATIM (regenerate nothing).
		if full["SUBDOMAIN"] != "trips" || full["DB_PASSWORD"] != "pw" || full["SECRET_KEY"] != "deadbeef" {
			t.Errorf("full env wrong: %v", full)
		}
	})

	t.Run("data_key missing — FAIL CLOSED (refuse)", func(t *testing.T) {
		recovered := map[string]string{"DB_PASSWORD": "pw"} // SECRET_KEY (a data_key) is gone
		full, _, err := reconcileRestoreSecrets(nonSecret, recovered,
			[]string{"DB_PASSWORD", "SECRET_KEY"}, []string{"SECRET_KEY"})
		if err == nil {
			t.Fatal("expected fail-closed error for missing data-encrypting key, got nil")
		}
		if full != nil {
			t.Errorf("full env should be nil on refusal, got %v", full)
		}
	})

	t.Run("data_key empty value — FAIL CLOSED", func(t *testing.T) {
		recovered := map[string]string{"SECRET_KEY": ""} // present but empty == unrecoverable
		_, _, err := reconcileRestoreSecrets(nonSecret, recovered, []string{"SECRET_KEY"}, []string{"SECRET_KEY"})
		if err == nil {
			t.Fatal("empty data-key value must fail closed")
		}
	})

	t.Run("resettable secret missing — proceed with warning", func(t *testing.T) {
		recovered := map[string]string{"SECRET_KEY": "deadbeef"} // data_key ok; DB_PASSWORD missing
		full, missing, err := reconcileRestoreSecrets(nonSecret, recovered,
			[]string{"DB_PASSWORD", "SECRET_KEY"}, []string{"SECRET_KEY"})
		if err != nil {
			t.Fatalf("a missing resettable secret must NOT fail closed: %v", err)
		}
		if len(missing) != 1 || missing[0] != "DB_PASSWORD" {
			t.Errorf("missing should be [DB_PASSWORD], got %v", missing)
		}
		if full["SECRET_KEY"] != "deadbeef" {
			t.Errorf("data-key should be preserved verbatim: %v", full)
		}
		if _, present := full["DB_PASSWORD"]; present {
			t.Errorf("missing resettable secret should be absent, not regenerated")
		}
	})
}
