package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"gorm.io/gorm"

	clusterrecovery "github.com/pingcap-inc/clinic/pkg/cluster_recovery"
	"github.com/pingcap-inc/clinic/pkg/db"
)

type fakeCommandSource struct {
	resolved *clusterrecovery.ResolvedSource
	calls    int
}

func (s *fakeCommandSource) Resolve(_ context.Context, candidate clusterrecovery.Candidate) (*clusterrecovery.ResolvedSource, error) {
	s.calls++
	if s.resolved == nil {
		return nil, errors.New("unexpected source read")
	}
	copyResolved := *s.resolved
	copyResolved.Candidate = candidate
	return &copyResolved, nil
}

type fakeCommandStore struct {
	snapshot    *clusterrecovery.Snapshot
	readCalls   int
	createCalls int
	updateCalls int
}

func (s *fakeCommandStore) ReadSnapshot(context.Context, clusterrecovery.ObjectKind, uint64, string) (*clusterrecovery.Snapshot, error) {
	s.readCalls++
	return s.snapshot, nil
}

func (s *fakeCommandStore) ApplyCreate(context.Context, clusterrecovery.ObjectKind, *db.Cluster, string) (*clusterrecovery.MutationResult, error) {
	s.createCalls++
	return &clusterrecovery.MutationResult{
		Outcome:       clusterrecovery.MutationOutcomeApplied,
		AfterChecksum: strings.Repeat("d", 64),
	}, nil
}

func (s *fakeCommandStore) ApplyUpdate(context.Context, clusterrecovery.ObjectKind, *db.Cluster, string, string, []string) (*clusterrecovery.MutationResult, error) {
	s.updateCalls++
	return nil, errors.New("unexpected update")
}

func TestPlanWritesPrivateArtifactsAndRedactedSummary(t *testing.T) {
	dir := canonicalTempDir(t)
	seedPath := filepath.Join(dir, "seed.json")
	writePrivateJSON(t, seedPath, testSeed("123456789"))
	source := &fakeCommandSource{}
	store := &fakeCommandStore{}
	loaderCalls := 0
	loader := func(string) (*commandRuntime, error) {
		loaderCalls++
		return &commandRuntime{
			environment: clusterrecovery.StagingEnvironment,
			source:      source,
			readStore:   store,
			writeStore:  store,
		}, nil
	}
	var stdout, stderr bytes.Buffer
	code := execute(context.Background(), []string{
		"plan",
		"--config", "/staging/config.yaml",
		"--seed", seedPath,
		"--output-dir", dir,
	}, &stdout, &stderr, loader)
	if code != exitOK || stderr.Len() != 0 || loaderCalls != 1 {
		t.Fatalf("code=%d stdout=%q stderr=%q loader=%d", code, stdout.String(), stderr.String(), loaderCalls)
	}
	if source.calls != 0 || store.readCalls != 0 {
		t.Fatalf("unsupported candidate reached dependencies: source=%d store=%d", source.calls, store.readCalls)
	}
	if strings.Contains(stdout.String(), "123456789") {
		t.Fatalf("summary leaked cluster ID: %s", stdout.String())
	}
	var summary planSummary
	if err := json.Unmarshal(stdout.Bytes(), &summary); err != nil {
		t.Fatal(err)
	}
	if summary.Counts[clusterrecovery.ClassificationSkipUnsupported] != 1 || summary.CreateActions != 0 || summary.UpdateActions != 0 {
		t.Fatalf("summary = %+v", summary)
	}
	paths := planArtifactPaths(dir, summary.RunID)
	for _, path := range paths {
		info, err := os.Stat(path)
		if err != nil {
			t.Fatal(err)
		}
		if info.Mode().Perm() != 0o600 {
			t.Fatalf("artifact %s mode = %o", path, info.Mode().Perm())
		}
	}
}

func TestPlanRejectsNonStagingRuntime(t *testing.T) {
	dir := canonicalTempDir(t)
	seedPath := filepath.Join(dir, "seed.json")
	writePrivateJSON(t, seedPath, testSeed("123456789"))
	loader := func(string) (*commandRuntime, error) {
		return &commandRuntime{
			environment: "prod",
			source:      &fakeCommandSource{},
			readStore:   &fakeCommandStore{},
		}, nil
	}
	var stdout, stderr bytes.Buffer
	code := execute(context.Background(), []string{
		"plan", "--config", "/prod/config.yaml", "--seed", seedPath, "--output-dir", dir,
	}, &stdout, &stderr, loader)
	if code != exitError || stderr.String() != "clinic recovery failed: environment_guard\n" {
		t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
	}
}

func TestApplyGuardRunsBeforeRuntimeInitialization(t *testing.T) {
	dir := canonicalTempDir(t)
	manifest := testCreateManifest(t)
	manifestPath := filepath.Join(dir, "manifest.json")
	writePrivateJSON(t, manifestPath, manifest)
	loaderCalls := 0
	loader := func(string) (*commandRuntime, error) {
		loaderCalls++
		return nil, errors.New("should not initialize")
	}
	var stdout, stderr bytes.Buffer
	code := execute(context.Background(), applyArguments(dir, manifestPath, manifest, false), &stdout, &stderr, loader)
	if code != exitError || loaderCalls != 0 || stderr.String() != "clinic recovery failed: apply_guard\n" {
		t.Fatalf("code=%d loader=%d stdout=%q stderr=%q", code, loaderCalls, stdout.String(), stderr.String())
	}
}

func TestApplyPreflightsReceiptBeforeRuntimeInitialization(t *testing.T) {
	dir := canonicalTempDir(t)
	manifest := testCreateManifest(t)
	manifestPath := filepath.Join(dir, "manifest.json")
	writePrivateJSON(t, manifestPath, manifest)
	receiptPath := filepath.Join(dir, "receipt.json")
	writePrivateJSON(t, receiptPath, map[string]bool{"occupied": true})
	loaderCalls := 0
	loader := func(string) (*commandRuntime, error) {
		loaderCalls++
		return nil, errors.New("should not initialize")
	}
	args := applyArguments(dir, manifestPath, manifest, true)
	var stdout, stderr bytes.Buffer
	code := execute(context.Background(), args, &stdout, &stderr, loader)
	if code != exitError || loaderCalls != 0 || stderr.String() != "clinic recovery failed: artifact_preflight_failed\n" {
		t.Fatalf("code=%d loader=%d stdout=%q stderr=%q", code, loaderCalls, stdout.String(), stderr.String())
	}
}

func TestApplyCanaryWritesRollbackAndReceipt(t *testing.T) {
	dir := canonicalTempDir(t)
	now := time.Now().UTC().Truncate(time.Second)
	desired := &db.Cluster{
		ClusterID:   "789456123",
		TenantID:    42,
		ClusterType: "cloud",
		Status:      "active",
		Name:        "sensitive-name",
		Provider:    "aws",
		Region:      "us-west-2",
	}
	source := &fakeCommandSource{resolved: &clusterrecovery.ResolvedSource{
		Desired:         desired,
		SourceUpdatedAt: now.Add(-time.Hour),
	}}
	store := &fakeCommandStore{snapshot: &clusterrecovery.Snapshot{
		Organization: &db.Organization{Model: dbModel(10), DbaasOrgID: 42, OrgType: "cloud"},
	}}
	seed := &clusterrecovery.Seed{
		SchemaVersion: clusterrecovery.SchemaVersion,
		Environment:   clusterrecovery.StagingEnvironment,
		IncidentStart: now.Add(-2 * time.Hour),
		IncidentEnd:   now.Add(-30 * time.Minute),
		Candidates: []clusterrecovery.SeedCandidate{{
			ClusterID:      desired.ClusterID,
			TenantID:       desired.TenantID,
			Evidence:       clusterrecovery.EvidenceSourceCompare,
			EventCreatedAt: now.Add(-time.Hour),
		}},
	}
	plan, err := clusterrecovery.NewPlanner(source, store).Plan(context.Background(), seed)
	if err != nil {
		t.Fatal(err)
	}
	if len(plan.CreateManifest.Actions) != 1 {
		t.Fatalf("create actions = %d", len(plan.CreateManifest.Actions))
	}
	manifestPath := filepath.Join(dir, "manifest.json")
	writePrivateJSON(t, manifestPath, &plan.CreateManifest)
	loader := func(string) (*commandRuntime, error) {
		return &commandRuntime{
			environment: clusterrecovery.StagingEnvironment,
			source:      source,
			readStore:   store,
			writeStore:  store,
		}, nil
	}
	var stdout, stderr bytes.Buffer
	code := execute(context.Background(), applyArguments(dir, manifestPath, &plan.CreateManifest, true), &stdout, &stderr, loader)
	if code != exitOK || stderr.Len() != 0 || store.createCalls != 1 {
		t.Fatalf("code=%d create=%d stdout=%q stderr=%q", code, store.createCalls, stdout.String(), stderr.String())
	}
	if strings.Contains(stdout.String(), desired.ClusterID) || strings.Contains(stdout.String(), desired.Name) {
		t.Fatalf("summary leaked customer metadata: %s", stdout.String())
	}
	for _, name := range []string{"rollback.json", "receipt.json"} {
		info, err := os.Stat(filepath.Join(dir, name))
		if err != nil {
			t.Fatal(err)
		}
		if info.Mode().Perm() != 0o600 {
			t.Fatalf("%s mode = %o", name, info.Mode().Perm())
		}
	}
}

func testSeed(clusterID string) *clusterrecovery.Seed {
	now := time.Date(2026, 8, 4, 6, 0, 0, 0, time.UTC)
	return &clusterrecovery.Seed{
		SchemaVersion: clusterrecovery.SchemaVersion,
		Environment:   clusterrecovery.StagingEnvironment,
		IncidentStart: now.Add(-2 * time.Hour),
		IncidentEnd:   now,
		Candidates: []clusterrecovery.SeedCandidate{{
			ClusterID:      clusterID,
			TenantID:       42,
			ClusterType:    "intentionally-unsupported",
			Evidence:       clusterrecovery.EvidenceSourceCompare,
			EventCreatedAt: now.Add(-time.Hour),
		}},
	}
}

func testCreateManifest(t *testing.T) *clusterrecovery.Manifest {
	t.Helper()
	now := time.Now().UTC()
	manifest := &clusterrecovery.Manifest{
		SchemaVersion: clusterrecovery.SchemaVersion,
		Environment:   clusterrecovery.StagingEnvironment,
		RunID:         "staging-cli-guard-test",
		MutationClass: clusterrecovery.MutationCreate,
		IncidentStart: now.Add(-2 * time.Hour),
		IncidentEnd:   now.Add(-time.Hour),
		GeneratedAt:   now,
		ExpiresAt:     now.Add(time.Hour),
		Actions: []clusterrecovery.Action{{
			Kind:            clusterrecovery.ObjectKindMgmtCluster,
			ClusterID:       "1",
			TenantID:        2,
			SourceUpdatedAt: now.Add(-time.Hour),
			DesiredChecksum: strings.Repeat("a", 64),
		}},
	}
	if err := clusterrecovery.FinalizeManifest(manifest); err != nil {
		t.Fatal(err)
	}
	return manifest
}

func applyArguments(dir, manifestPath string, manifest *clusterrecovery.Manifest, allowWrite bool) []string {
	args := []string{
		"apply",
		"--config", "/staging/config.yaml",
		"--manifest", manifestPath,
		"--expected-manifest-hash", manifest.ManifestHash,
		"--approval-ref", "reviewed-pr-123",
		"--workload-identity", "system:serviceaccount:clinic-staging:recovery",
		"--mutation-class", string(manifest.MutationClass),
		"--phase", "canary",
		"--offset", "0",
		"--limit", "1",
		"--rollback", filepath.Join(dir, "rollback.json"),
		"--receipt", filepath.Join(dir, "receipt.json"),
	}
	if allowWrite {
		args = append(args, "--allow-write")
	}
	return args
}

func writePrivateJSON(t *testing.T, path string, value any) {
	t.Helper()
	payload, err := json.Marshal(value)
	if err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, payload, 0o600); err != nil {
		t.Fatal(err)
	}
}

func canonicalTempDir(t *testing.T) string {
	t.Helper()
	dir, err := filepath.EvalSymlinks(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	return dir
}

// dbModel keeps GORM-specific test setup local to this file.
func dbModel(id uint) gorm.Model {
	return gorm.Model{ID: id}
}
