package backup

import (
	"bytes"
	"log"
	"path/filepath"
	"strings"
	"testing"
)

// newSecretGenUnit lays out a recovery unit whose manifest names one resettable secret
// (DB_PASSWORD) and one data-key (SECRET_KEY) — the O4 test fixture.
func newSecretGenUnit(t *testing.T) (drive string) {
	t.Helper()
	drive = filepath.Join(t.TempDir(), "drive")
	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
}

// TestRestoreGeneratesMissingResettableSecret proves Scenario F: with the data-key recovered but
// DB_PASSWORD unrecoverable, the restore PROCEEDS and RecreateStackFromUnit receives a NON-EMPTY
// generated DB_PASSWORD (pre-O4 it was simply absent → compose deployed blank → exit 1). Also
// proves the generator is consulted for the resettable secret ONLY — never a data-key — and that
// the generated VALUE never reaches the logs.
// COMPANION red-proof: reverting to the pre-fix behaviour (no generation) fails the non-empty
// DB_PASSWORD assertion.
func TestRestoreGeneratesMissingResettableSecret(t *testing.T) {
	const genValue = "generated-secret-value-do-not-log"
	drive := newSecretGenUnit(t)
	fake := &fakeRecoveryProvider{
		hdd:     drive,
		running: true,
		secrets: map[string]string{"SECRET_KEY": "deadbeef"}, // DB_PASSWORD unrecoverable
	}
	var logBuf bytes.Buffer
	m := &Manager{logger: log.New(&logBuf, "", 0), systemDataPath: filepath.Join(drive, "..", "sys"),
		stackProvider: fake}
	var genCalls []string
	m.generateSecret = func(stackName, envVar string) (string, bool) {
		genCalls = append(genCalls, envVar)
		return genValue, true
	}

	if err := m.RestoreFromRecoveryUnit("app"); err != nil {
		t.Fatalf("restore must proceed for a missing RESETTABLE secret: %v", err)
	}
	if fake.gotEnv == nil {
		t.Fatal("recreate was not called")
	}
	if fake.gotEnv["DB_PASSWORD"] != genValue {
		t.Errorf("DB_PASSWORD = %q, want the generated replacement (pre-O4: absent → blank deploy)", fake.gotEnv["DB_PASSWORD"])
	}
	if fake.gotEnv["SECRET_KEY"] != "deadbeef" {
		t.Errorf("recovered data-key must pass through verbatim, got %q", fake.gotEnv["SECRET_KEY"])
	}
	if len(genCalls) != 1 || genCalls[0] != "DB_PASSWORD" {
		t.Errorf("generator consulted for %v, want exactly [DB_PASSWORD] (never data-keys)", genCalls)
	}
	logs := logBuf.String()
	if !strings.Contains(logs, "generated replacement") || !strings.Contains(logs, "DB_PASSWORD") {
		t.Errorf("WARN must name the reset credential; logs:\n%s", logs)
	}
	// Secrets safety: the generated VALUE must never be logged — names only.
	if strings.Contains(logs, genValue) {
		t.Errorf("SECRET LEAK: generated value found in logs:\n%s", logs)
	}
}

// TestRestoreProceedsWhenNoGenerator proves Scenario G: a missing resettable secret with NO
// generator (seam returns ok=false, or no seam wired) still proceeds — env var absent, loud WARN
// that the app may fail to start — and never invents a default.
func TestRestoreProceedsWhenNoGenerator(t *testing.T) {
	run := func(t *testing.T, wire bool) {
		drive := newSecretGenUnit(t)
		fake := &fakeRecoveryProvider{
			hdd:     drive,
			running: true,
			secrets: map[string]string{"SECRET_KEY": "deadbeef"},
		}
		var logBuf bytes.Buffer
		m := &Manager{logger: log.New(&logBuf, "", 0), systemDataPath: filepath.Join(drive, "..", "sys"),
			stackProvider: fake}
		if wire {
			m.generateSecret = func(string, string) (string, bool) { return "", false } // no spec (Scenario G)
		}

		if err := m.RestoreFromRecoveryUnit("app"); err != nil {
			t.Fatalf("restore must still proceed: %v", err)
		}
		if _, present := fake.gotEnv["DB_PASSWORD"]; present {
			t.Errorf("no generator → the secret must stay absent, not be invented: %v", fake.gotEnv)
		}
		logs := logBuf.String()
		if !strings.Contains(logs, "no generator") || !strings.Contains(logs, "may fail to start") || !strings.Contains(logs, "DB_PASSWORD") {
			t.Errorf("upgraded WARN must name the var and the may-fail consequence; logs:\n%s", logs)
		}
	}
	t.Run("generator wired, field has no spec", func(t *testing.T) { run(t, true) })
	t.Run("no generator wired at all", func(t *testing.T) { run(t, false) })
}

// TestRestoreGenerationNeverReachesDataKeys proves the frozen gate is untouched by O4: a missing
// DATA-KEY still refuses fail-closed BEFORE any generation — the generator is never consulted and
// the app is never recreated, even with a generator eagerly offering values.
func TestRestoreGenerationNeverReachesDataKeys(t *testing.T) {
	drive := newSecretGenUnit(t)
	fake := &fakeRecoveryProvider{
		hdd:     drive,
		secrets: map[string]string{"DB_PASSWORD": "pw"}, // SECRET_KEY (data_key) missing
	}
	m := &Manager{logger: log.New(bytes.NewBuffer(nil), "", 0), systemDataPath: filepath.Join(drive, "..", "sys"),
		stackProvider: fake}
	var genCalls []string
	m.generateSecret = func(_, envVar string) (string, bool) {
		genCalls = append(genCalls, envVar)
		return "eager-value", true
	}

	if err := m.RestoreFromRecoveryUnit("app"); err == nil {
		t.Fatal("missing data-key must still refuse fail-closed")
	}
	if len(genCalls) != 0 {
		t.Errorf("generator consulted for %v — must be unreachable when the gate refuses", genCalls)
	}
	if fake.gotEnv != nil {
		t.Errorf("recreate must not run on refusal: %v", fake.gotEnv)
	}
}
