package cvt

import (
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	corev1 "k8s.io/api/core/v1"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	apiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
)

func TestConvertCRRFPhase(t *testing.T) {
	cases := map[string]struct {
		input cloudv1.CRRFPhase
		want  apiv1.CRRFInfo_CRRFPhase
	}{
		"Pending maps to PENDING": {
			input: cloudv1.CRRFPhasePending,
			want:  apiv1.CRRFInfo_PENDING,
		},
		"Running maps to RUNNING": {
			input: cloudv1.CRRFPhaseRunning,
			want:  apiv1.CRRFInfo_RUNNING,
		},
		"Destroying maps to DESTROYING": {
			input: cloudv1.CRRFPhaseDestroying,
			want:  apiv1.CRRFInfo_DESTROYING,
		},
		"Failed maps to FAILED": {
			input: cloudv1.CRRFPhaseFailed,
			want:  apiv1.CRRFInfo_FAILED,
		},
		"Unknown phase maps to UNSPECIFIED": {
			input: cloudv1.CRRFPhase("UnknownState"),
			want:  apiv1.CRRFInfo_CRRF_PHASE_UNSPECIFIED,
		},
		"Empty phase maps to UNSPECIFIED": {
			input: cloudv1.CRRFPhase(""),
			want:  apiv1.CRRFInfo_CRRF_PHASE_UNSPECIFIED,
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			got := convertCRRFPhase(tc.input)
			if got != tc.want {
				t.Errorf("convertCRRFPhase(%q) = %v, want %v", tc.input, got, tc.want)
			}
		})
	}
}

func mustMarshalCRRFStatus(s cloudv1.CRRFStatus) string {
	b, err := json.Marshal(s)
	if err != nil {
		panic(err)
	}
	return string(b)
}

func mustMarshalCRRFSpec(s cloudv1.CRRFSpec) string {
	b, err := json.Marshal(s)
	if err != nil {
		panic(err)
	}
	return string(b)
}

func TestCRRFStatus2PB(t *testing.T) {
	cases := map[string]struct {
		annotations    map[string]string
		backupStorage  *string
		wantPhase      apiv1.CRRFInfo_CRRFPhase
		wantMessage    string
		wantRelease    string
		wantError      bool
		wantCrrfId     string
		wantUpstream   string
		wantDownstream string
		wantImage      string
	}{
		"no annotations returns UNSPECIFIED phase": {
			annotations: nil,
			wantPhase:   apiv1.CRRFInfo_CRRF_PHASE_UNSPECIFIED,
		},
		"only crrf-spec annotation returns PENDING": {
			annotations: map[string]string{
				cloudv1.BackupAnnotationKeyCRRFSpec: mustMarshalCRRFSpec(cloudv1.CRRFSpec{
					CRRFID: "crrf-1",
				}),
			},
			wantPhase:  apiv1.CRRFInfo_PENDING,
			wantCrrfId: "crrf-1",
		},
		"spec-only path populates upstream from backup storage": {
			annotations: map[string]string{
				cloudv1.BackupAnnotationKeyCRRFSpec: mustMarshalCRRFSpec(cloudv1.CRRFSpec{
					CRRFID:            "crrf-1",
					DownstreamStorage: "s3://dst/",
					Image:             "pingcap/br:v7",
				}),
			},
			backupStorage:  strPtr("s3://upstream-bucket/"),
			wantPhase:      apiv1.CRRFInfo_PENDING,
			wantCrrfId:     "crrf-1",
			wantUpstream:   "s3://upstream-bucket/",
			wantDownstream: "s3://dst/",
			wantImage:      "pingcap/br:v7",
		},
		"crrf-status Running is returned": {
			annotations: map[string]string{
				cloudv1.BackupAnnotationKeyCRRFStatus: mustMarshalCRRFStatus(cloudv1.CRRFStatus{
					CRRFID:             "crrf-1",
					Phase:              cloudv1.CRRFPhaseRunning,
					HelmReleaseName:    "crrf-daemon-my-backup",
					LastTransitionTime: metav1.Now(),
				}),
			},
			wantPhase:   apiv1.CRRFInfo_RUNNING,
			wantRelease: "crrf-daemon-my-backup",
			wantCrrfId:  "crrf-1",
		},
		"crrf-status Running includes storage and image fields": {
			annotations: map[string]string{
				cloudv1.BackupAnnotationKeyCRRFStatus: mustMarshalCRRFStatus(cloudv1.CRRFStatus{
					CRRFID:            "crrf-1",
					Phase:             cloudv1.CRRFPhaseRunning,
					HelmReleaseName:   "crrf-daemon-my-backup",
					UpstreamStorage:   "s3://upstream/",
					DownstreamStorage: "s3://downstream/",
					Image:             "pingcap/br:v7.5.0",
				}),
			},
			wantPhase:      apiv1.CRRFInfo_RUNNING,
			wantRelease:    "crrf-daemon-my-backup",
			wantCrrfId:     "crrf-1",
			wantUpstream:   "s3://upstream/",
			wantDownstream: "s3://downstream/",
			wantImage:      "pingcap/br:v7.5.0",
		},
		"crrf-status Failed includes error message": {
			annotations: map[string]string{
				cloudv1.BackupAnnotationKeyCRRFStatus: mustMarshalCRRFStatus(cloudv1.CRRFStatus{
					CRRFID:             "crrf-1",
					Phase:              cloudv1.CRRFPhaseFailed,
					Message:            "helm install failed: timeout",
					HelmReleaseName:    "crrf-daemon-my-backup",
					LastTransitionTime: metav1.Now(),
				}),
			},
			wantPhase:   apiv1.CRRFInfo_FAILED,
			wantMessage: "helm install failed: timeout",
			wantRelease: "crrf-daemon-my-backup",
			wantCrrfId:  "crrf-1",
		},
		"crrf-status takes precedence over crrf-spec": {
			annotations: map[string]string{
				cloudv1.BackupAnnotationKeyCRRFSpec: mustMarshalCRRFSpec(cloudv1.CRRFSpec{
					CRRFID: "crrf-1",
				}),
				cloudv1.BackupAnnotationKeyCRRFStatus: mustMarshalCRRFStatus(cloudv1.CRRFStatus{
					CRRFID:             "crrf-1",
					Phase:              cloudv1.CRRFPhaseRunning,
					HelmReleaseName:    "crrf-daemon-my-backup",
					LastTransitionTime: metav1.Now(),
				}),
			},
			wantPhase:   apiv1.CRRFInfo_RUNNING,
			wantRelease: "crrf-daemon-my-backup",
			wantCrrfId:  "crrf-1",
		},
		"corrupt crrf-status annotation returns error": {
			annotations: map[string]string{
				cloudv1.BackupAnnotationKeyCRRFSpec: mustMarshalCRRFSpec(cloudv1.CRRFSpec{
					CRRFID: "crrf-1",
				}),
				cloudv1.BackupAnnotationKeyCRRFStatus: "not-valid-json{{{",
			},
			wantPhase: apiv1.CRRFInfo_CRRF_PHASE_UNSPECIFIED,
			wantError: true,
		},
		"corrupt crrf-status with no spec returns error": {
			annotations: map[string]string{
				cloudv1.BackupAnnotationKeyCRRFStatus: "not-valid-json{{{",
			},
			wantPhase: apiv1.CRRFInfo_CRRF_PHASE_UNSPECIFIED,
			wantError: true,
		},
		"crrf-status Destroying is returned": {
			annotations: map[string]string{
				cloudv1.BackupAnnotationKeyCRRFStatus: mustMarshalCRRFStatus(cloudv1.CRRFStatus{
					CRRFID:             "crrf-1",
					Phase:              cloudv1.CRRFPhaseDestroying,
					HelmReleaseName:    "crrf-daemon-my-backup",
					LastTransitionTime: metav1.Now(),
				}),
			},
			wantPhase:   apiv1.CRRFInfo_DESTROYING,
			wantRelease: "crrf-daemon-my-backup",
			wantCrrfId:  "crrf-1",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			backup := &cloudv1.Backup{
				ObjectMeta: metav1.ObjectMeta{
					Name:        "my-backup",
					Annotations: tc.annotations,
				},
			}
			if tc.backupStorage != nil {
				backup.Spec.Storage = tc.backupStorage
			}
			got, err := CRRFStatus2PB("cluster-123", "my-log-backup", backup)
			if err != nil {
				if !tc.wantError {
					t.Fatalf("CRRFStatus2PB returned unexpected error: %v", err)
				}
				return
			}
			if tc.wantError {
				t.Fatal("expected error but got nil")
			}

			if got.CrrfId != tc.wantCrrfId {
				t.Errorf("CrrfId = %q, want %q", got.CrrfId, tc.wantCrrfId)
			}
			if got.ClusterId != "cluster-123" {
				t.Errorf("ClusterId = %q, want %q", got.ClusterId, "cluster-123")
			}
			if got.LogBackupName != "my-log-backup" {
				t.Errorf("LogBackupName = %q, want %q", got.LogBackupName, "my-log-backup")
			}
			if got.Phase != tc.wantPhase {
				t.Errorf("Phase = %v, want %v", got.Phase, tc.wantPhase)
			}
			if got.Message != tc.wantMessage {
				t.Errorf("Message = %q, want %q", got.Message, tc.wantMessage)
			}
			if got.HelmReleaseName != tc.wantRelease {
				t.Errorf("HelmReleaseName = %q, want %q", got.HelmReleaseName, tc.wantRelease)
			}
			if got.UpstreamStorage != tc.wantUpstream {
				t.Errorf("UpstreamStorage = %q, want %q", got.UpstreamStorage, tc.wantUpstream)
			}
			if got.DownstreamStorage != tc.wantDownstream {
				t.Errorf("DownstreamStorage = %q, want %q", got.DownstreamStorage, tc.wantDownstream)
			}
			if got.Image != tc.wantImage {
				t.Errorf("Image = %q, want %q", got.Image, tc.wantImage)
			}
		})
	}
}

func TestLogBackup2PB_MapsPhases(t *testing.T) {
	t.Parallel()

	cases := map[string]struct {
		phase cloudv1.BackupConditionType
		want  apiv1.LogBackup_Status
	}{
		"failed":     {phase: cloudv1.BackupFailed, want: apiv1.LogBackup_FAILED},
		"running":    {phase: cloudv1.BackupRunning, want: apiv1.LogBackup_RUNNING},
		"stopped":    {phase: cloudv1.BackupStopped, want: apiv1.LogBackup_STOPPED},
		"prepare":    {phase: cloudv1.BackupPrepare, want: apiv1.LogBackup_PREPARE},
		"scheduled":  {phase: cloudv1.BackupScheduled, want: apiv1.LogBackup_SCHEDULED},
		"truncating": {phase: cloudv1.BackupTruncating, want: apiv1.LogBackup_TRUNCATING},
		"truncated":  {phase: cloudv1.BackupTruncated, want: apiv1.LogBackup_TRUNCATED},
		"stopping":   {phase: cloudv1.BackupStopping, want: apiv1.LogBackup_STOPPING},
		"unknown":    {phase: cloudv1.BackupConditionType("mystery"), want: apiv1.LogBackup_STATUS_UNSPECIFIED},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			pb := LogBackup2PB("log-ab-1", &cloudv1.Backup{
				Status: cloudv1.BackupStatus{Phase: tc.phase},
			})
			require.Equal(t, tc.want, pb.LogStatus)
		})
	}
}

func TestLogBackup2PB_PropagatesSubCommandStatuses(t *testing.T) {
	t.Helper()

	failedAt := time.Date(2026, 4, 29, 16, 5, 9, 0, time.UTC)
	backup := &cloudv1.Backup{
		Status: cloudv1.BackupStatus{
			Phase:            cloudv1.BackupRunning,
			BackupTS:         "458142299193344000",
			BackupPath:       "gcs://bucket/path",
			LogCheckpointTs:  "466221498642989182",
			LogTruncateUntil: "465773218328215644",
			LogSubCommandStatuses: map[cloudv1.LogSubCommandType]cloudv1.LogSubCommandStatus{
				cloudv1.LogTruncateCommand: {
					Command:            cloudv1.LogTruncateCommand,
					LogTruncatingUntil: "465773218328215644",
					Phase:              cloudv1.BackupFailed,
					TimeStarted:        metav1.NewTime(failedAt.Add(-2 * time.Minute)),
					TimeCompleted:      metav1.NewTime(failedAt),
					Conditions: []cloudv1.BackupCondition{
						{
							Command:            cloudv1.LogTruncateCommand,
							Type:               cloudv1.BackupFailed,
							Status:             corev1.ConditionTrue,
							LastTransitionTime: metav1.NewTime(failedAt),
							Reason:             "TruncateLogBackupFailed",
							Message:            "googleapi: Error 503",
						},
						{
							Command:            cloudv1.LogUnknownCommand,
							Type:               cloudv1.BackupConditionType("odd"),
							Status:             corev1.ConditionUnknown,
							LastTransitionTime: metav1.Time{},
							Reason:             "Unknown",
							Message:            "fallback branch",
						},
					},
				},
				cloudv1.LogResumeCommand: {
					Command: cloudv1.LogResumeCommand,
					Phase:   cloudv1.BackupRunning,
				},
			},
		},
	}

	pb := LogBackup2PB("log-ab-1", backup)

	require.Equal(t, apiv1.LogBackup_RUNNING, pb.LogStatus)
	require.Len(t, pb.LogSubCommandStatuses, 2)
	require.Contains(t, pb.LogSubCommandStatuses, "log-truncate")

	truncate := pb.LogSubCommandStatuses["log-truncate"]
	require.Equal(t, apiv1.LogBackup_LOG_TRUNCATE, truncate.Command)
	require.Equal(t, apiv1.LogBackup_FAILED, truncate.Phase)
	require.Equal(t, "465773218328215644", truncate.LogTruncatingUntil)
	require.NotNil(t, truncate.TimeStarted)
	require.NotNil(t, truncate.TimeCompleted)
	require.Len(t, truncate.Conditions, 2)
	require.Equal(t, apiv1.LogBackup_LOG_TRUNCATE, truncate.Conditions[0].Command)
	require.Equal(t, apiv1.LogBackup_FAILED, truncate.Conditions[0].Type)
	require.Equal(t, apiv1.ClusterCondition_True, truncate.Conditions[0].Status)
	require.Equal(t, "TruncateLogBackupFailed", truncate.Conditions[0].Reason)
	require.Equal(t, "googleapi: Error 503", truncate.Conditions[0].Message)
	require.True(t, truncate.Conditions[0].LastTransitionTime.AsTime().Equal(failedAt))
	require.Equal(t, apiv1.LogBackup_LOG_UNKNOWN, truncate.Conditions[1].Command)
	require.Equal(t, apiv1.LogBackup_STATUS_UNSPECIFIED, truncate.Conditions[1].Type)
	require.Equal(t, apiv1.ClusterCondition_CONDITION_STATUS_UNSPECIFIED, truncate.Conditions[1].Status)
	require.NotNil(t, truncate.Conditions[1].LastTransitionTime)

	resume := pb.LogSubCommandStatuses["log-resume"]
	require.Equal(t, apiv1.LogBackup_LOG_RESUME, resume.Command)
	require.Equal(t, apiv1.LogBackup_RUNNING, resume.Phase)
	require.Nil(t, resume.TimeStarted)
	require.Nil(t, resume.TimeCompleted)
}

func TestLogBackup2PB_LeavesSubCommandStatusesNilWhenEmpty(t *testing.T) {
	t.Parallel()

	pb := LogBackup2PB("log-ab-1", &cloudv1.Backup{})
	require.Nil(t, pb.LogSubCommandStatuses)
}

func TestLogSubCommandTypeToPB(t *testing.T) {
	t.Parallel()

	cases := map[string]struct {
		input cloudv1.LogSubCommandType
		want  apiv1.LogBackup_SubCommandType
	}{
		"log-start":    {input: cloudv1.LogStartCommand, want: apiv1.LogBackup_LOG_START},
		"log-truncate": {input: cloudv1.LogTruncateCommand, want: apiv1.LogBackup_LOG_TRUNCATE},
		"log-stop":     {input: cloudv1.LogStopCommand, want: apiv1.LogBackup_LOG_STOP},
		"log-pause":    {input: cloudv1.LogPauseCommand, want: apiv1.LogBackup_LOG_PAUSE},
		"log-resume":   {input: cloudv1.LogResumeCommand, want: apiv1.LogBackup_LOG_RESUME},
		"unknown":      {input: cloudv1.LogSubCommandType("mystery"), want: apiv1.LogBackup_LOG_UNKNOWN},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			require.Equal(t, tc.want, logSubCommandTypeToPB(tc.input))
		})
	}
}

func TestConditionStatusToPB(t *testing.T) {
	t.Parallel()

	cases := map[string]struct {
		input corev1.ConditionStatus
		want  apiv1.ClusterCondition_ConditionStatus
	}{
		"true":    {input: corev1.ConditionTrue, want: apiv1.ClusterCondition_True},
		"false":   {input: corev1.ConditionFalse, want: apiv1.ClusterCondition_False},
		"unknown": {input: corev1.ConditionUnknown, want: apiv1.ClusterCondition_CONDITION_STATUS_UNSPECIFIED},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			require.Equal(t, tc.want, conditionStatusToPB(tc.input))
		})
	}
}

func strPtr(s string) *string { return &s }
