package app

import (
	"bytes"
	"encoding/json"
	"os"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/helmfile/vals"
	"github.com/stretchr/testify/assert"
	"go.uber.org/zap"

	ffs "github.com/helmfile/helmfile/pkg/filesystem"
	"github.com/helmfile/helmfile/pkg/helmexec"
	"github.com/helmfile/helmfile/pkg/testhelper"
	"github.com/helmfile/helmfile/pkg/testutil"
)

func testListWithEnvironment(t *testing.T, cfg configImpl) {
	type testcase struct {
		environment string
		ns          string
		error       string
		selectors   []string
		expected    string
	}

	check := func(t *testing.T, tc testcase, cfg configImpl) {
		t.Helper()

		bs := runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
			t.Helper()

			valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
			if err != nil {
				t.Errorf("unexpected error creating vals runtime: %v", err)
			}

			files := map[string]string{
				"/path/to/helmfile.d/helmfile_1.yaml": `
environments:
  development: {}
  shared: {}
---
releases:
- name: logging
  chart: incubator/raw
  namespace: kube-system

- name: kubernetes-external-secrets
  chart: incubator/raw
  namespace: kube-system
  needs:
  - kube-system/logging

- name: external-secrets
  chart: incubator/raw
  namespace: default
  labels:
    app: test
  needs:
  - kube-system/kubernetes-external-secrets

- name: my-release
  chart: incubator/raw
  namespace: default
  labels:
    app: test
  needs:
  - default/external-secrets


# Disabled releases are treated as missing
- name: disabled
  chart: incubator/raw
  namespace: kube-system
  installed: false

- name: test2
  chart: incubator/raw
  needs:
  - kube-system/disabled

- name: test3
  chart: incubator/raw
  needs:
  - test2
`,
				"/path/to/helmfile.d/helmfile_2.yaml": `
environments:
  test: {}
  shared: {}
---
repositories:
- name: bitnami
  url: https://charts.bitnami.com/bitnami

releases:
- name: cache
  namespace: my-app
  chart: bitnami/redis
  version: 17.0.7
  labels:
    app: test

- name: database
  namespace: my-app
  chart: bitnami/postgres
  version: 11.6.22
`,
				"/path/to/helmfile.d/helmfile_3.yaml": `
releases:
- name: global
  chart: incubator/raw
  namespace: kube-system
`,
			}

			app := appWithFs(&App{
				OverrideHelmBinary:              DefaultHelmBinary,
				fs:                              ffs.DefaultFileSystem(),
				OverrideKubeContext:             "default",
				DisableKubeVersionAutoDetection: true,
				Env:                             tc.environment,
				Logger:                          logger,
				valsRuntime:                     valsRuntime,
			}, files)

			expectNoCallsToHelm(app)

			if tc.ns != "" {
				app.Namespace = tc.ns
			}

			if tc.selectors != nil {
				app.Selectors = tc.selectors
			}

			var listErr error
			out, err := testutil.CaptureStdout(func() {
				listErr = app.ListReleases(cfg)
			})
			assert.NoError(t, err)

			var gotErr string
			if listErr != nil {
				gotErr = listErr.Error()
			}

			if d := cmp.Diff(tc.error, gotErr); d != "" {
				t.Fatalf("unexpected error: want (-), got (+): %s", d)
			}

			assert.Equal(t, tc.expected, out)
		})

		testhelper.RequireLog(t, "app_list_test", bs)
	}

	t.Run("default environment includes all releases", func(t *testing.T) {
		check(t, testcase{
			environment: "default",
			expected: `NAME                       	NAMESPACE  	ENABLED	INSTALLED	LABELS                                                          	CHART           	VERSION
test2                      	           	true   	true     	chart:raw,name:test2,namespace:                                 	incubator/raw
test3                      	           	true   	true     	chart:raw,name:test3,namespace:                                 	incubator/raw
external-secrets           	default    	true   	true     	app:test,chart:raw,name:external-secrets,namespace:default      	incubator/raw
my-release                 	default    	true   	true     	app:test,chart:raw,name:my-release,namespace:default            	incubator/raw
disabled                   	kube-system	true   	false    	chart:raw,name:disabled,namespace:kube-system                   	incubator/raw
global                     	kube-system	true   	true     	chart:raw,name:global,namespace:kube-system                     	incubator/raw
kubernetes-external-secrets	kube-system	true   	true     	chart:raw,name:kubernetes-external-secrets,namespace:kube-system	incubator/raw
logging                    	kube-system	true   	true     	chart:raw,name:logging,namespace:kube-system                    	incubator/raw
cache                      	my-app     	true   	true     	app:test,chart:redis,name:cache,namespace:my-app                	bitnami/redis   	17.0.7
database                   	my-app     	true   	true     	chart:postgres,name:database,namespace:my-app                   	bitnami/postgres	11.6.22
`,
		}, cfg)
	})

	t.Run("fail on unknown environment", func(t *testing.T) {
		check(t, testcase{
			environment: "staging",
			error:       `err: no releases found that matches specified selector() and environment(staging), in any helmfile`,
		}, cfg)
	})

	t.Run("list releases matching selector and environment", func(t *testing.T) {
		check(t, testcase{
			environment: "development",
			selectors:   []string{"app=test"},
			expected: `NAME            	NAMESPACE	ENABLED	INSTALLED	LABELS                                                    	CHART        	VERSION
external-secrets	default  	true   	true     	app:test,chart:raw,name:external-secrets,namespace:default	incubator/raw
my-release      	default  	true   	true     	app:test,chart:raw,name:my-release,namespace:default      	incubator/raw
`,
		}, cfg)
	})

	t.Run("filters releases for environment used in one file only", func(t *testing.T) {
		check(t, testcase{
			environment: "test",
			expected: `NAME    	NAMESPACE	ENABLED	INSTALLED	LABELS                                          	CHART           	VERSION
cache   	my-app   	true   	true     	app:test,chart:redis,name:cache,namespace:my-app	bitnami/redis   	17.0.7
database	my-app   	true   	true     	chart:postgres,name:database,namespace:my-app   	bitnami/postgres	11.6.22
`,
		}, cfg)
	})

	t.Run("filters releases for environment used in multiple files", func(t *testing.T) {
		check(t, testcase{
			environment: "shared",
			// 'global' release has no environments, so is still excluded
			expected: `NAME                       	NAMESPACE  	ENABLED	INSTALLED	LABELS                                                          	CHART           	VERSION
test2                      	           	true   	true     	chart:raw,name:test2,namespace:                                 	incubator/raw
test3                      	           	true   	true     	chart:raw,name:test3,namespace:                                 	incubator/raw
external-secrets           	default    	true   	true     	app:test,chart:raw,name:external-secrets,namespace:default      	incubator/raw
my-release                 	default    	true   	true     	app:test,chart:raw,name:my-release,namespace:default            	incubator/raw
disabled                   	kube-system	true   	false    	chart:raw,name:disabled,namespace:kube-system                   	incubator/raw
kubernetes-external-secrets	kube-system	true   	true     	chart:raw,name:kubernetes-external-secrets,namespace:kube-system	incubator/raw
logging                    	kube-system	true   	true     	chart:raw,name:logging,namespace:kube-system                    	incubator/raw
cache                      	my-app     	true   	true     	app:test,chart:redis,name:cache,namespace:my-app                	bitnami/redis   	17.0.7
database                   	my-app     	true   	true     	chart:postgres,name:database,namespace:my-app                   	bitnami/postgres	11.6.22
`,
		}, cfg)
	})
}

func TestListWithEnvironment(t *testing.T) {
	t.Run("with skipCharts=false", func(t *testing.T) {
		testListWithEnvironment(t, configImpl{skipCharts: false})
	})
	t.Run("with skipCharts=true", func(t *testing.T) {
		testListWithEnvironment(t, configImpl{skipCharts: true})
	})
}

func testListWithJSONOutput(t *testing.T, cfg configImpl) {
	cfg.output = "json"

	files := map[string]string{
		"/path/to/helmfile.d/first.yaml": `
environments:
  default:
    values:
     - myrelease2:
         enabled: false
---
releases:
- name: myrelease1
  chart: mychart1
  installed: false
  labels:
    id: myrelease1
- name: myrelease2
  chart: mychart1
  condition: myrelease2.enabled
`,
		"/path/to/helmfile.d/second.yaml": `
releases:
- name: myrelease3
  chart: mychart1
  installed: true
- name: myrelease4
  chart: mychart1
  labels:
    id: myrelease1
`,
	}
	stdout := os.Stdout
	defer func() { os.Stdout = stdout }()

	var buffer bytes.Buffer
	syncWriter := testhelper.NewSyncWriter(&buffer)
	logger := helmexec.NewLogger(syncWriter, "debug")

	app := appWithFs(&App{
		OverrideHelmBinary:              DefaultHelmBinary,
		fs:                              ffs.DefaultFileSystem(),
		OverrideKubeContext:             "default",
		DisableKubeVersionAutoDetection: true,
		Env:                             "default",
		Logger:                          logger,
		Namespace:                       "testNamespace",
	}, files)

	expectNoCallsToHelm(app)

	out, err := testutil.CaptureStdout(func() {
		err := app.ListReleases(cfg)
		assert.Nil(t, err)
	})
	assert.NoError(t, err)

	expected := `[{"name":"myrelease1","namespace":"testNamespace","enabled":true,"installed":false,"labels":"chart:mychart1,id:myrelease1,name:myrelease1,namespace:testNamespace","chart":"mychart1","version":""},{"name":"myrelease2","namespace":"testNamespace","enabled":false,"installed":true,"labels":"chart:mychart1,name:myrelease2,namespace:testNamespace","chart":"mychart1","version":""},{"name":"myrelease3","namespace":"testNamespace","enabled":true,"installed":true,"labels":"chart:mychart1,name:myrelease3,namespace:testNamespace","chart":"mychart1","version":""},{"name":"myrelease4","namespace":"testNamespace","enabled":true,"installed":true,"labels":"chart:mychart1,id:myrelease1,name:myrelease4,namespace:testNamespace","chart":"mychart1","version":""}]
`
	assert.Equal(t, expected, out)
}

func TestListWithJSONOutput(t *testing.T) {
	t.Run("with skipCharts=false", func(t *testing.T) {
		testListWithJSONOutput(t, configImpl{skipCharts: false})
	})
	t.Run("with skipCharts=true", func(t *testing.T) {
		testListWithJSONOutput(t, configImpl{skipCharts: true})
	})
}

func TestListWithLockFileVersion(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.yaml": `
repositories:
- name: bitnami
  url: https://charts.bitnami.com/bitnami

releases:
- name: redis
  namespace: default
  chart: bitnami/redis
  version: ">=1.0.0"
`,
		"/path/to/helmfile.lock": `version: v0.0.0
digest: sha256:abc123
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: redis
  repository: https://charts.bitnami.com/bitnami
  version: 17.0.7
`,
	}

	stdout := os.Stdout
	defer func() { os.Stdout = stdout }()

	var buffer bytes.Buffer
	syncWriter := testhelper.NewSyncWriter(&buffer)
	logger := helmexec.NewLogger(syncWriter, "debug")

	valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
	if err != nil {
		t.Fatalf("unexpected error creating vals runtime: %v", err)
	}

	app := appWithFs(&App{
		OverrideHelmBinary:              DefaultHelmBinary,
		fs:                              ffs.DefaultFileSystem(),
		OverrideKubeContext:             "default",
		DisableKubeVersionAutoDetection: true,
		Env:                             "default",
		Logger:                          logger,
		valsRuntime:                     valsRuntime,
	}, files)

	expectNoCallsToHelm(app)

	out, err := testutil.CaptureStdout(func() {
		err := app.ListReleases(configImpl{skipCharts: true, output: "json"})
		assert.Nil(t, err)
	})
	assert.NoError(t, err)

	var releases []HelmRelease
	if err := json.Unmarshal([]byte(out), &releases); err != nil {
		t.Fatalf("failed to parse JSON output: %v", err)
	}

	assert.Len(t, releases, 1, "expected 1 release")
	assert.Equal(t, "redis", releases[0].Name)
	assert.Equal(t, "bitnami/redis", releases[0].Chart)
	assert.Equal(t, "17.0.7", releases[0].Version, "expected version from helmfile.lock")
}

func TestListWithLockFileVersion_MultiFile(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.d/first.yaml": `
repositories:
- name: bitnami
  url: https://charts.bitnami.com/bitnami

releases:
- name: redis
  namespace: default
  chart: bitnami/redis
  version: ">=1.0.0"
`,
		"/path/to/helmfile.d/first.lock": `version: v0.0.0
digest: sha256:abc123
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: redis
  repository: https://charts.bitnami.com/bitnami
  version: 17.0.7
`,
		"/path/to/helmfile.d/second.yaml": `
repositories:
- name: bitnami
  url: https://charts.bitnami.com/bitnami

releases:
- name: nginx
  namespace: default
  chart: bitnami/nginx
  version: ">=1.0.0"
`,
		"/path/to/helmfile.d/second.lock": `version: v0.0.0
digest: sha256:def456
generated: "2024-01-01T00:00:00Z"
dependencies:
- name: nginx
  repository: https://charts.bitnami.com/bitnami
  version: 15.0.0
`,
	}

	stdout := os.Stdout
	defer func() { os.Stdout = stdout }()

	var buffer bytes.Buffer
	syncWriter := testhelper.NewSyncWriter(&buffer)
	logger := helmexec.NewLogger(syncWriter, "debug")

	valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
	if err != nil {
		t.Fatalf("unexpected error creating vals runtime: %v", err)
	}

	app := appWithFs(&App{
		OverrideHelmBinary:              DefaultHelmBinary,
		fs:                              ffs.DefaultFileSystem(),
		OverrideKubeContext:             "default",
		DisableKubeVersionAutoDetection: true,
		Env:                             "default",
		Logger:                          logger,
		valsRuntime:                     valsRuntime,
	}, files)

	expectNoCallsToHelm(app)

	out, err := testutil.CaptureStdout(func() {
		err := app.ListReleases(configImpl{skipCharts: true, output: "json"})
		assert.Nil(t, err)
	})
	assert.NoError(t, err)

	var releases []HelmRelease
	if err := json.Unmarshal([]byte(out), &releases); err != nil {
		t.Fatalf("failed to parse JSON output: %v", err)
	}

	assert.Len(t, releases, 2, "expected 2 releases")

	releaseMap := make(map[string]HelmRelease)
	for _, r := range releases {
		releaseMap[r.Name] = r
	}

	redis := releaseMap["redis"]
	assert.Equal(t, "bitnami/redis", redis.Chart)
	assert.Equal(t, "17.0.7", redis.Version, "expected redis version from first.lock")

	nginx := releaseMap["nginx"]
	assert.Equal(t, "bitnami/nginx", nginx.Chart)
	assert.Equal(t, "15.0.0", nginx.Version, "expected nginx version from second.lock")
}
