package app

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

	"github.com/helmfile/vals"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/yaml.v3"

	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 TestPrintEnv_SingleHelmfile_YAML(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  production:
    kubeContext: prod-cluster
    values:
      - region: us-east-1
        environment: production
        debug: false
---
releases: []
`,
	}

	app := createTestApp(t, files, "production")
	cfg := configImpl{output: "yaml"}

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

	// Parse YAML output
	var result map[string]any
	err = yaml.Unmarshal([]byte(out), &result)
	require.NoError(t, err)

	// Verify structure
	assert.Equal(t, "production", result["name"])
	assert.Equal(t, "prod-cluster", result["kubeContext"])
	assert.Contains(t, result["filePath"], "helmfile.yaml")

	// Verify values
	values, ok := result["values"].(map[string]any)
	require.True(t, ok, "values should be a map")
	assert.Equal(t, "us-east-1", values["region"])
	assert.Equal(t, "production", values["environment"])
	assert.Equal(t, false, values["debug"])
}

func TestPrintEnv_SingleHelmfile_JSON(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  staging:
    kubeContext: staging-cluster
    values:
      - database:
          host: db.staging.local
          port: 5432
---
releases: []
`,
	}

	app := createTestApp(t, files, "staging")
	cfg := configImpl{output: "json"}

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

	// Parse JSON output (should be an array)
	var results []map[string]any
	err = json.Unmarshal([]byte(out), &results)
	require.NoError(t, err)
	require.Len(t, results, 1, "should have one environment")

	result := results[0]
	assert.Equal(t, "staging", result["name"])
	assert.Equal(t, "staging-cluster", result["kubeContext"])
	assert.Contains(t, result["filePath"], "helmfile.yaml")

	// Verify nested values
	values, ok := result["values"].(map[string]any)
	require.True(t, ok)
	database, ok := values["database"].(map[string]any)
	require.True(t, ok)
	assert.Equal(t, "db.staging.local", database["host"])
	assert.Equal(t, float64(5432), database["port"]) // JSON numbers are float64
}

func TestPrintEnv_MultipleHelmfiles_YAML(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  dev:
    kubeContext: main-context
    values:
      - source: main
        sharedValue: from-main
---
helmfiles:
  - path: sub/helmfile.yaml
releases: []
`,
		"/path/to/sub/helmfile.yaml": `
environments:
  dev:
    kubeContext: sub-context
    values:
      - source: sub
        subValue: from-sub
---
releases: []
`,
	}

	app := createTestApp(t, files, "dev")
	cfg := configImpl{output: "yaml"}

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

	// Split by --- to get individual documents
	docs := strings.Split(out, "---\n")
	// Filter out empty documents
	var nonEmptyDocs []string
	for _, doc := range docs {
		trimmed := strings.TrimSpace(doc)
		if trimmed != "" {
			nonEmptyDocs = append(nonEmptyDocs, trimmed)
		}
	}

	assert.GreaterOrEqual(t, len(nonEmptyDocs), 2, "should have at least 2 environment documents")

	// Verify each document is valid YAML
	for i, doc := range nonEmptyDocs {
		var result map[string]any
		err := yaml.Unmarshal([]byte(doc), &result)
		require.NoError(t, err, "document %d should be valid YAML", i)
		assert.Equal(t, "dev", result["name"], "document %d should have correct name", i)
		assert.Contains(t, result, "kubeContext", "document %d should have kubeContext", i)
		assert.Contains(t, result, "values", "document %d should have values", i)
		assert.Contains(t, result, "filePath", "document %d should have filePath", i)
	}
}

func TestPrintEnv_MultipleHelmfiles_JSON(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  test:
    kubeContext: main-kube
    values:
      - mainKey: mainValue
---
helmfiles:
  - path: child1/helmfile.yaml
  - path: child2/helmfile.yaml
releases: []
`,
		"/path/to/child1/helmfile.yaml": `
environments:
  test:
    kubeContext: child1-kube
    values:
      - child1Key: child1Value
---
releases: []
`,
		"/path/to/child2/helmfile.yaml": `
environments:
  test:
    kubeContext: child2-kube
    values:
      - child2Key: child2Value
---
releases: []
`,
	}

	app := createTestApp(t, files, "test")
	cfg := configImpl{output: "json"}

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

	// Parse JSON array
	var results []map[string]any
	err = json.Unmarshal([]byte(out), &results)
	require.NoError(t, err)
	assert.GreaterOrEqual(t, len(results), 3, "should have at least 3 environments")

	// Verify all have correct structure
	for i, result := range results {
		assert.Equal(t, "test", result["name"], "result %d should have name 'test'", i)
		assert.Contains(t, result, "kubeContext", "result %d should have kubeContext", i)
		assert.Contains(t, result, "values", "result %d should have values", i)
		assert.Contains(t, result, "filePath", "result %d should have filePath", i)
	}
}

func TestPrintEnv_WithDefaults(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  default:
    values:
      - color: blue
  prod:
    values:
      - color: red
        size: large
---
releases: []
`,
	}

	app := createTestApp(t, files, "prod")
	cfg := configImpl{output: "json"}

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

	var results []map[string]any
	err = json.Unmarshal([]byte(out), &results)
	require.NoError(t, err)
	require.Len(t, results, 1)

	values, ok := results[0]["values"].(map[string]any)
	require.True(t, ok)

	// Should have values from prod environment
	assert.Equal(t, "red", values["color"])
	assert.Equal(t, "large", values["size"])
}

func TestPrintEnv_InvalidOutputFormat(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  dev:
    values:
      - test: value
---
releases: []
`,
	}

	app := createTestApp(t, files, "dev")
	cfg := configImpl{output: "xml"} // Invalid format

	_, err := testutil.CaptureStdout(func() {
		err := app.PrintEnv(cfg)
		assert.Error(t, err)
		assert.Contains(t, err.Error(), "unsupported output format")
	})
	require.NoError(t, err)
}

func TestPrintEnv_EmptyValues(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  minimal:
    kubeContext: minimal-cluster
---
releases: []
`,
	}

	app := createTestApp(t, files, "minimal")
	cfg := configImpl{output: "json"}

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

	var results []map[string]any
	err = json.Unmarshal([]byte(out), &results)
	require.NoError(t, err)
	require.Len(t, results, 1)

	result := results[0]
	assert.Equal(t, "minimal", result["name"])
	assert.Equal(t, "minimal-cluster", result["kubeContext"])

	// Values should exist but be empty
	values, ok := result["values"].(map[string]any)
	require.True(t, ok)
	assert.Empty(t, values)
}

func TestPrintEnv_NoKubeContext(t *testing.T) {
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  local:
    values:
      - app: myapp
---
releases: []
`,
	}

	app := createTestApp(t, files, "local")
	cfg := configImpl{output: "yaml"}

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

	var result map[string]any
	err = yaml.Unmarshal([]byte(out), &result)
	require.NoError(t, err)

	assert.Equal(t, "local", result["name"])
	// kubeContext should NOT be present when empty (omitted from output)
	_, exists := result["kubeContext"]
	assert.False(t, exists)
}

func TestPrintEnv_DefaultOutput(t *testing.T) {
	// When output is empty string, should default to YAML
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  dev:
    values:
      - key: value
---
releases: []
`,
	}

	app := createTestApp(t, files, "dev")
	cfg := configImpl{output: ""} // Empty output should default to yaml

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

	// Should be valid YAML
	var result map[string]any
	err = yaml.Unmarshal([]byte(out), &result)
	require.NoError(t, err, "empty output format should default to YAML")
}

func TestPrintEnv_UndefinedEnvironment(t *testing.T) {
	// Test behavior with undefined environment
	files := map[string]string{
		"/path/to/helmfile.yaml": `
environments:
  production:
    values:
      - env: prod
---
releases: []
`,
	}

	app := createTestApp(t, files, "staging") // staging is not defined
	cfg := configImpl{output: "json"}

	out, err := testutil.CaptureStdout(func() {
		err := app.PrintEnv(cfg)
		// The behavior depends on helmfile's environment handling
		// It may succeed with empty values or fail
		if err != nil {
			assert.Contains(t, err.Error(), "environment")
		}
	})
	require.NoError(t, err)

	// If no error, output should be valid JSON (potentially empty array)
	if out != "" {
		var results []map[string]any
		err = json.Unmarshal([]byte(out), &results)
		// Should either be valid JSON or empty
		if err != nil {
			assert.Equal(t, "", out, "if not valid JSON, output should be empty")
		}
	}
}

// Helper function to create test app with common setup
func createTestApp(t *testing.T, files map[string]string, environment string) *App {
	t.Helper()

	valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
	require.NoError(t, err)

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

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

	expectNoCallsToHelm(app)

	return app
}
