package opsservice

import (
	"context"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/observability/pkg/common/message"
)

// fakeCommandExecutor is a test double for commandExecutor that records calls
// and returns canned output. Temp file paths are normalized to VALUES before
// lookup so tests remain independent of runtime temp-file paths.
type fakeCommandExecutor struct {
	calls  []string
	output map[string][]byte
	errs   map[string]error
}

func (e *fakeCommandExecutor) Run(_ context.Context, name string, args ...string) ([]byte, error) {
	normalized := normalizeArgs(name, args)
	e.calls = append(e.calls, normalized)
	if err := e.errs[normalized]; err != nil {
		return nil, err
	}
	return e.output[normalized], nil
}

// normalizeArgs replaces any --values <path> argument with "--values VALUES"
// so that canned output/error maps are independent of temp-file paths.
func normalizeArgs(name string, args []string) string {
	out := make([]string, len(args))
	copy(out, args)
	for i, a := range out {
		if i > 0 && out[i-1] == "--values" && strings.Contains(a, string('/')) {
			out[i] = "VALUES"
		}
	}
	return name + " " + strings.Join(out, " ")
}

func TestCommandHelmReleaseDiffRunner_ReturnsChangedDiff(t *testing.T) {
	exec := &fakeCommandExecutor{
		output: map[string][]byte{
			"helm diff upgrade alertmanager-regional oci://registry.example.com/observability-charts/alertmanager --namespace observability --allow-unreleased --version 1.2.3 --values VALUES": []byte("+ changed"),
			"helm show chart oci://registry.example.com/observability-charts/alertmanager --version 1.2.3":                                                                                      []byte("digest: sha256:resolved\n"),
		},
	}
	runner := commandHelmReleaseDiffRunner{executor: exec, helmBinary: "helm"}

	diff, err := runner.Diff(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
		Config: message.HelmReleaseConfig{
			Version: "1.2.3",
			Values:  map[string]interface{}{"replicaCount": 1},
			SourceRef: &message.HelmReleaseSourceRef{
				URL: "oci://registry.example.com/observability-charts",
			},
		},
	})

	require.NoError(t, err)
	assert.True(t, diff.Changed)
	assert.Equal(t, "+ changed", diff.Output)
	assert.Equal(t, "sha256:resolved", diff.ResolvedDigest)
	assert.Contains(t, exec.calls[0], "oci://registry.example.com/observability-charts/alertmanager")
}

func TestCommandHelmReleaseDiffRunner_ReturnsUnchangedDiff(t *testing.T) {
	exec := &fakeCommandExecutor{
		output: map[string][]byte{
			"helm diff upgrade alertmanager-regional oci://registry.example.com/observability-charts/alertmanager --namespace observability --allow-unreleased --version 1.2.3": []byte(""),
			"helm show chart oci://registry.example.com/observability-charts/alertmanager --version 1.2.3":                                                                      []byte("digest: sha256:abc\n"),
		},
	}
	runner := commandHelmReleaseDiffRunner{executor: exec, helmBinary: "helm"}

	diff, err := runner.Diff(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
		Config: message.HelmReleaseConfig{
			Version: "1.2.3",
			SourceRef: &message.HelmReleaseSourceRef{
				URL: "oci://registry.example.com/observability-charts",
			},
		},
	})

	require.NoError(t, err)
	assert.False(t, diff.Changed)
	assert.Equal(t, "", diff.Output)
	assert.Equal(t, "sha256:abc", diff.ResolvedDigest)
}

func TestCommandHelmReleaseDiffRunner_HelmDiffError(t *testing.T) {
	exec := &fakeCommandExecutor{
		errs: map[string]error{
			"helm diff upgrade alertmanager-regional oci://registry.example.com/observability-charts/alertmanager --namespace observability --allow-unreleased --version 1.2.3": assert.AnError,
		},
	}
	runner := commandHelmReleaseDiffRunner{executor: exec, helmBinary: "helm"}

	_, err := runner.Diff(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
		Config: message.HelmReleaseConfig{
			Version: "1.2.3",
			SourceRef: &message.HelmReleaseSourceRef{
				URL: "oci://registry.example.com/observability-charts",
			},
		},
	})

	require.Error(t, err)
}

func TestCommandHelmReleaseDiffRunner_HelmShowError(t *testing.T) {
	exec := &fakeCommandExecutor{
		output: map[string][]byte{
			"helm diff upgrade alertmanager-regional oci://registry.example.com/observability-charts/alertmanager --namespace observability --allow-unreleased --version 1.2.3": []byte(""),
		},
		errs: map[string]error{
			"helm show chart oci://registry.example.com/observability-charts/alertmanager --version 1.2.3": assert.AnError,
		},
	}
	runner := commandHelmReleaseDiffRunner{executor: exec, helmBinary: "helm"}

	_, err := runner.Diff(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
		Config: message.HelmReleaseConfig{
			Version: "1.2.3",
			SourceRef: &message.HelmReleaseSourceRef{
				URL: "oci://registry.example.com/observability-charts",
			},
		},
	})

	require.Error(t, err)
}

func TestCommandHelmReleaseDiffRunner_NoDigestLineIsEmpty(t *testing.T) {
	exec := &fakeCommandExecutor{
		output: map[string][]byte{
			"helm diff upgrade alertmanager-regional oci://registry.example.com/observability-charts/alertmanager --namespace observability --allow-unreleased --version 1.2.3": []byte(""),
			"helm show chart oci://registry.example.com/observability-charts/alertmanager --version 1.2.3":                                                                      []byte("name: alertmanager\nversion: 1.2.3\n"),
		},
	}
	runner := commandHelmReleaseDiffRunner{executor: exec, helmBinary: "helm"}

	diff, err := runner.Diff(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
		Config: message.HelmReleaseConfig{
			Version: "1.2.3",
			SourceRef: &message.HelmReleaseSourceRef{
				URL: "oci://registry.example.com/observability-charts",
			},
		},
	})

	require.NoError(t, err)
	assert.Equal(t, "", diff.ResolvedDigest)
}

func TestCommandHelmReleaseDiffRunner_UsesSourceRefURLForChartReference(t *testing.T) {
	exec := &fakeCommandExecutor{
		output: map[string][]byte{
			"helm diff upgrade alertmanager-regional oci://registry.example.com/charts/alertmanager --namespace observability --allow-unreleased --version 1.2.3": []byte(""),
			"helm show chart oci://registry.example.com/charts/alertmanager --version 1.2.3":                                                                      []byte("digest: sha256:resolved\n"),
		},
	}
	runner := commandHelmReleaseDiffRunner{executor: exec, helmBinary: "helm"}

	diff, err := runner.Diff(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
		Config: message.HelmReleaseConfig{
			Version: "1.2.3",
			SourceRef: &message.HelmReleaseSourceRef{
				URL: "oci://registry.example.com/charts",
			},
		},
	})

	require.NoError(t, err)
	assert.Equal(t, "sha256:resolved", diff.ResolvedDigest)
	assert.Contains(t, exec.calls[0], "oci://registry.example.com/charts/alertmanager")
}

func TestCommandHelmReleaseDiffRunner_ErrorWhenSourceRefURLMissing(t *testing.T) {
	exec := &fakeCommandExecutor{}
	runner := commandHelmReleaseDiffRunner{executor: exec, helmBinary: "helm"}

	_, err := runner.Diff(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
		Config:    message.HelmReleaseConfig{Version: "1.2.3"},
	})

	require.Error(t, err)
	assert.Contains(t, err.Error(), "config.sourceRef.url is required for helm diff")
	assert.Empty(t, exec.calls)
}

type fakeHelmReleaseDiffRunner struct {
	result message.HelmReleaseRenderedDiff
	err    error
}

func (r fakeHelmReleaseDiffRunner) Diff(context.Context, message.DiffHelmReleaseReq) (message.HelmReleaseRenderedDiff, error) {
	return r.result, r.err
}

func TestDiffHelmRelease_RejectsMissingExactVersion(t *testing.T) {
	svc := &HelmReleaseService{diffRunner: fakeHelmReleaseDiffRunner{}}

	resp, err := svc.DiffHelmRelease(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
	})

	require.NoError(t, err)
	assert.False(t, resp.SafeToApply)
	assert.Contains(t, resp.BlockingReasons, "config.version must be an exact chart version")
}

func TestDiffHelmRelease_BlocksMissingSourceRefURL(t *testing.T) {
	// The fake runner is wired to return an error; if DiffHelmRelease invokes it
	// the outer error would propagate and require.NoError would catch it.
	// No error here proves the runner was never called.
	svc := &HelmReleaseService{diffRunner: fakeHelmReleaseDiffRunner{err: assert.AnError}}

	for _, tc := range []struct {
		name   string
		srcRef *message.HelmReleaseSourceRef
	}{
		{"nil sourceRef", nil},
		{"empty url", &message.HelmReleaseSourceRef{Kind: "HelmRepository"}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			resp, err := svc.DiffHelmRelease(context.Background(), message.DiffHelmReleaseReq{
				Name:      "alertmanager-regional",
				Namespace: "observability",
				Component: "alertmanager",
				Config: message.HelmReleaseConfig{
					Version:   "1.2.3",
					SourceRef: tc.srcRef,
				},
			})

			require.NoError(t, err)
			assert.False(t, resp.SafeToApply)
			assert.Contains(t, resp.BlockingReasons, "config.sourceRef.url is required for helm diff")
		})
	}
}

func TestDiffHelmRelease_BlocksDigestMismatch(t *testing.T) {
	svc := &HelmReleaseService{diffRunner: fakeHelmReleaseDiffRunner{
		result: message.HelmReleaseRenderedDiff{
			Changed:        false,
			ResolvedDigest: "sha256:resolved",
		},
	}}

	resp, err := svc.DiffHelmRelease(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
		Config: message.HelmReleaseConfig{
			Version:        "1.2.3",
			ExpectedDigest: "sha256:expected",
			SourceRef:      &message.HelmReleaseSourceRef{URL: "oci://registry.example.com/observability-charts"},
		},
	})

	require.NoError(t, err)
	assert.False(t, resp.SafeToApply)
	assert.Contains(t, resp.BlockingReasons, "expected digest sha256:expected does not match resolved digest sha256:resolved")
}

func TestDiffHelmRelease_AllowsNoDiffWithMatchingDigest(t *testing.T) {
	svc := &HelmReleaseService{diffRunner: fakeHelmReleaseDiffRunner{
		result: message.HelmReleaseRenderedDiff{
			Changed:        false,
			Output:         "",
			ResolvedDigest: "sha256:abc",
		},
	}}

	resp, err := svc.DiffHelmRelease(context.Background(), message.DiffHelmReleaseReq{
		Name:      "alertmanager-regional",
		Namespace: "observability",
		Component: "alertmanager",
		Config: message.HelmReleaseConfig{
			Version:        "1.2.3",
			ExpectedDigest: "sha256:abc",
			SourceRef:      &message.HelmReleaseSourceRef{URL: "oci://registry.example.com/observability-charts"},
		},
	})

	require.NoError(t, err)
	assert.True(t, resp.SafeToApply)
	assert.False(t, resp.RenderedDiff.Changed)
	assert.Empty(t, resp.BlockingReasons)
}

func TestDiffHelmRelease_AdoptionModeWarnsOnOwnershipMetadata(t *testing.T) {
	svc := &HelmReleaseService{diffRunner: fakeHelmReleaseDiffRunner{
		result: message.HelmReleaseRenderedDiff{Changed: false},
	}}

	resp, err := svc.DiffHelmRelease(context.Background(), message.DiffHelmReleaseReq{
		Name:         "regional-server",
		Namespace:    "observability",
		Component:    "regional-server",
		AdoptionMode: true,
		Config: message.HelmReleaseConfig{
			Version:   "0.1.0",
			SourceRef: &message.HelmReleaseSourceRef{URL: "oci://registry.example.com/observability-charts"},
		},
	})

	require.NoError(t, err)
	assert.True(t, resp.SafeToApply)
	assert.Contains(t, resp.Warnings, "adoption mode requires metadata-only live resource ownership patches before reconcile is enabled")
}
