package backup

import (
	"context"
	"os"
	"strings"
	"testing"
	"time"

	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/config"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/keymanager"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/mlkem768"
	"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/x25519"
	"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
)

// TestRunOnce_MissingKeys verifies that RunOnce surfaces a configuration error
// when the public key paths are not provided, exercising the early validation
// path (registry build, keymanager construction, key load).
func TestRunOnce_MissingKeys(t *testing.T) {
	cfg := &config.Config{}
	// PQPublicKeyPath and ClassicalPublicKeyPath intentionally left empty.

	err := RunOnce(context.Background(), cfg)
	if err == nil {
		t.Fatal("RunOnce returned nil error with empty config, want error")
	}

	if !strings.Contains(err.Error(), "load pq public key") {
		t.Fatalf("RunOnce error = %q, want it to mention %q", err, "load pq public key")
	}
}

// TestRunOnce_PQKeyNotFound verifies the error returned when the post-quantum
// public key path is set but the file does not exist on disk.
func TestRunOnce_PQKeyNotFound(t *testing.T) {
	cfg := &config.Config{
		PQScheme: 0x0006,
		// Path points to a file that does not exist; use t.TempDir() to keep
		// the test hermetic regardless of the working directory.
		PQPublicKeyPath:        t.TempDir() + "/does-not-exist-pq.pem",
		ClassicalPublicKeyPath: t.TempDir() + "/does-not-exist-classical.pem",
	}

	err := RunOnce(context.Background(), cfg)
	if err == nil {
		t.Fatal("RunOnce returned nil error when keys are missing on disk, want error")
	}

	if !strings.Contains(err.Error(), "load pq public key") {
		t.Fatalf("RunOnce error = %q, want it to mention %q", err, "load pq public key")
	}
}

// TestRunOnce_ValidKeys_PgDumpMissing generates real dual-KEM keys, builds a
// fully valid config, and calls RunOnce.  Since pg_dump is not installed in the
// test environment, the pipeline fails at the dump step, giving coverage of
// the full orchestration path (registry, keymanager, encryptor, sink, runner,
// pgDumpOpts assembly) while still asserting the expected error.
func TestRunOnce_ValidKeys_PgDumpMissing(t *testing.T) {
	dir := t.TempDir()
	keyDir := dir + "/keys"
	backupDir := dir + "/backups"
	_ = os.MkdirAll(keyDir, 0o755)
	_ = os.MkdirAll(backupDir, 0o755)

	reg := crypto.NewRegistry()
	_ = reg.Register(0x0006, func() crypto.KEM { return mlkem768.New() })
	_ = reg.Register(0x0007, func() crypto.KEM { return x25519.New() })

	km := keymanager.NewKeyManager(reg)

	pqPubFile, _ := os.Create(keyDir + "/pq.pub.pem")
	pqPrivFile, _ := os.Create(keyDir + "/pq.priv.pem")
	_ = km.Generate(0x0006, pqPubFile, pqPrivFile, nil)
	_ = pqPubFile.Close()
	_ = pqPrivFile.Close()

	classicalPubFile, _ := os.Create(keyDir + "/classical.pub.pem")
	classicalPrivFile, _ := os.Create(keyDir + "/classical.priv.pem")
	_ = km.Generate(0x0007, classicalPubFile, classicalPrivFile, nil)
	_ = classicalPubFile.Close()
	_ = classicalPrivFile.Close()

	cfg := &config.Config{
		PQScheme:               0x0006,
		ClassicalScheme:        0x0007,
		PQPublicKeyPath:        keyDir + "/pq.pub.pem",
		ClassicalPublicKeyPath: keyDir + "/classical.pub.pem",
		Backup: struct {
			Dir           string `mapstructure:"dir"`
			RetentionDays int    `mapstructure:"retention_days"`
			Cron          string `mapstructure:"cron"`
		}{
			Dir:           backupDir,
			RetentionDays: 180,
		},
		PG: struct {
			Host          string   `mapstructure:"host"`
			Port          int      `mapstructure:"port"`
			User          string   `mapstructure:"user"`
			Password      string   `mapstructure:"password"`
			Database      string   `mapstructure:"database"`
			SSLMode       string   `mapstructure:"sslmode"`
			ExcludeTables []string `mapstructure:"exclude_tables"`
		}{
			Host:          "localhost",
			Port:          5432,
			User:          "test",
			Password:      "test",
			Database:      "test",
			ExcludeTables: []string{"e2e_one_time_keys_json"},
		},
	}

	err := RunOnce(context.Background(), cfg)
	if err == nil {
		t.Fatal("expected error because pg_dump is not installed in the test environment")
	}
	if !strings.Contains(err.Error(), "backup pipeline failed") {
		t.Fatalf("expected error containing 'backup pipeline failed', got: %v", err)
	}

	entries, _ := os.ReadDir(backupDir)
	for _, e := range entries {
		if strings.HasSuffix(e.Name(), ".tmp") {
			t.Fatalf("unexpected .tmp file after failed backup: %s", e.Name())
		}
	}
}

func TestArtifactKey(t *testing.T) {
	ts := time.Date(2025, 1, 2, 15, 4, 5, 0, time.UTC)
	key := ArtifactKey(ts)

	wantPrefix := "synapse-20250102-150405-"
	if !strings.HasPrefix(key, wantPrefix) {
		t.Errorf("key = %q, want prefix %q", key, wantPrefix)
	}
	if !strings.HasSuffix(key, ".dump.pqenc") {
		t.Errorf("key = %q, want suffix .dump.pqenc", key)
	}

	secondKey := ArtifactKey(ts)
	if key == secondKey {
		t.Errorf("two keys for the same timestamp collide: %q", key)
	}
}
