package state

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"

	"go.uber.org/zap"

	"github.com/helmfile/helmfile/pkg/filesystem"
	"github.com/helmfile/helmfile/pkg/yaml"
)

func TestRewriteChartDependencies(t *testing.T) {
	tests := []struct {
		name           string
		chartYaml      string
		expectModified bool
		expectError    bool
		validate       func(t *testing.T, modifiedChartYaml string)
	}{
		{
			name:           "no Chart.yaml exists",
			chartYaml:      "",
			expectModified: false,
			expectError:    false,
		},
		{
			name: "no dependencies",
			chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
`,
			expectModified: false,
			expectError:    false,
		},
		{
			name: "absolute file:// dependency - not modified",
			chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
  - name: dep1
    repository: file:///absolute/path/to/chart
    version: 1.0.0
`,
			expectModified: false,
			expectError:    false,
			validate: func(t *testing.T, modifiedChartYaml string) {
				if !strings.Contains(modifiedChartYaml, "file:///absolute/path/to/chart") {
					t.Errorf("absolute path should not be modified")
				}
			},
		},
		{
			name: "relative file:// dependency - should be modified",
			chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
  - name: dep1
    repository: file://../relative-chart
    version: 1.0.0
`,
			expectModified: true,
			expectError:    false,
			validate: func(t *testing.T, modifiedChartYaml string) {
				if strings.Contains(modifiedChartYaml, "file://../relative-chart") {
					t.Errorf("relative path should have been converted to absolute")
				}

				if !strings.Contains(modifiedChartYaml, "file://") {
					t.Errorf("should still have file:// prefix")
				}
			},
		},
		{
			name: "mixed dependencies - only relative file:// modified",
			chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
  - name: dep1
    repository: https://charts.example.com
    version: 1.0.0
  - name: dep2
    repository: file://../relative-chart
    version: 2.0.0
  - name: dep3
    repository: file:///absolute/chart
    version: 3.0.0
  - name: dep4
    repository: oci://registry.example.com/charts/mychart
    version: 4.0.0
`,
			expectModified: true,
			expectError:    false,
			validate: func(t *testing.T, modifiedChartYaml string) {
				if !strings.Contains(modifiedChartYaml, "https://charts.example.com") {
					t.Errorf("https repository should not be modified")
				}

				if strings.Contains(modifiedChartYaml, "file://../relative-chart") {
					t.Errorf("relative file:// path should have been converted")
				}

				if !strings.Contains(modifiedChartYaml, "file:///absolute/chart") {
					t.Errorf("absolute file:// path should not be modified")
				}

				if !strings.Contains(modifiedChartYaml, "oci://registry.example.com") {
					t.Errorf("oci repository should not be modified")
				}
			},
		},
		{
			name: "multiple relative dependencies",
			chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
  - name: dep1
    repository: file://../chart1
    version: 1.0.0
  - name: dep2
    repository: file://./chart2
    version: 2.0.0
  - name: dep3
    repository: file://../../../chart3
    version: 3.0.0
`,
			expectModified: true,
			expectError:    false,
			validate: func(t *testing.T, modifiedChartYaml string) {
				if strings.Contains(modifiedChartYaml, "file://../chart1") ||
					strings.Contains(modifiedChartYaml, "file://./chart2") ||
					strings.Contains(modifiedChartYaml, "file://../../../chart3") {
					t.Errorf("all relative paths should have been converted")
				}
			},
		},
		{
			name: "extra fields",
			chartYaml: `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
  - name: dep1
    repository: https://charts.example.com
    version: 1.0.0
    condition: dep.install
    import-values:
    - child: persistence
      parent: global.persistence
    extra-field2: bbb
extra-field: aaa
`,
			expectModified: false,
			expectError:    false,
			validate: func(t *testing.T, modifiedChartYaml string) {
				requiredFields := []string{
					"condition: dep.install",
					"import-values:",
					"child: persistence",
					"parent: global.persistence",
					"extra-field2: bbb",
					"extra-field: aaa",
				}

				for _, field := range requiredFields {
					if !strings.Contains(modifiedChartYaml, field) {
						t.Errorf("field %q should be preserved", field)
					}
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tempDir, err := os.MkdirTemp("", "helmfile-test-")
			if err != nil {
				t.Fatalf("failed to create temp dir: %v", err)
			}
			defer os.RemoveAll(tempDir)

			if tt.chartYaml != "" {
				chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
				if err := os.WriteFile(chartYamlPath, []byte(tt.chartYaml), 0644); err != nil {
					t.Fatalf("failed to write Chart.yaml: %v", err)
				}
			}

			logger := zap.NewNop().Sugar()
			st := &HelmState{
				logger: logger,
				fs:     filesystem.DefaultFileSystem(),
			}

			rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)

			if tt.expectError {
				if err == nil {
					t.Errorf("expected error but got none")
				}
				return
			}

			if tt.expectModified {
				if rewrittenPath == tempDir {
					t.Errorf("expected rewrittenPath != tempDir when modifications are needed, got same path")
				}
			} else {
				if rewrittenPath != tempDir {
					t.Errorf("expected rewrittenPath == tempDir when no modifications are needed, got %q", rewrittenPath)
				}
			}

			if err != nil {
				t.Fatalf("unexpected error: %v", err)
			}
			defer cleanup()

			if tt.chartYaml == "" {
				return
			}

			modifiedChartBytes, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
			if err != nil {
				t.Fatalf("failed to read Chart.yaml: %v", err)
			}
			modifiedChartYaml := string(modifiedChartBytes)

			if tt.validate != nil {
				tt.validate(t, modifiedChartYaml)
			}
		})
	}
}

func TestRewriteChartDependencies_OriginalNotModified(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "helmfile-test-")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	originalChart := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
  - name: dep1
    repository: file://../relative-chart
    version: 1.0.0
`

	chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
	if err := os.WriteFile(chartYamlPath, []byte(originalChart), 0644); err != nil {
		t.Fatalf("failed to write Chart.yaml: %v", err)
	}

	logger := zap.NewNop().Sugar()
	st := &HelmState{
		logger: logger,
		fs:     filesystem.DefaultFileSystem(),
	}

	rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	t.Cleanup(func() {
		if rewrittenPath == tempDir {
			return
		}

		cleanup()

		if _, statErr := os.Stat(rewrittenPath); !os.IsNotExist(statErr) {
			t.Errorf("expected rewritten chart path %q to be removed after cleanup", rewrittenPath)
		}
	})

	if rewrittenPath == tempDir {
		t.Errorf("expected a different path when modifications are needed, got same path")
	}

	modifiedContent, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
	if err != nil {
		t.Fatalf("failed to read modified Chart.yaml: %v", err)
	}

	if string(modifiedContent) == originalChart {
		t.Errorf("Chart.yaml in the copy should have been modified")
	}

	originalContent, err := os.ReadFile(chartYamlPath)
	if err != nil {
		t.Fatalf("failed to read original Chart.yaml: %v", err)
	}

	if string(originalContent) != originalChart {
		t.Errorf("original Chart.yaml should not have been modified\nexpected:\n%s\ngot:\n%s",
			originalChart, string(originalContent))
	}
}

func TestRewriteChartDependencies_PreservesOtherFields(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "helmfile-test-")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	chartYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
description: A test chart
keywords:
  - test
  - example
maintainers:
  - name: Test User
    email: test@example.com
dependencies:
  - name: dep1
    repository: file://../relative-chart
    version: 1.0.0
    condition: test
`

	chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
	if err := os.WriteFile(chartYamlPath, []byte(chartYaml), 0644); err != nil {
		t.Fatalf("failed to write Chart.yaml: %v", err)
	}

	logger := zap.NewNop().Sugar()
	st := &HelmState{
		logger: logger,
		fs:     filesystem.DefaultFileSystem(),
	}

	rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	t.Cleanup(func() {
		if rewrittenPath == tempDir {
			return
		}

		cleanup()

		if _, statErr := os.Stat(rewrittenPath); !os.IsNotExist(statErr) {
			t.Errorf("expected rewritten chart path %q to be removed after cleanup", rewrittenPath)
		}
	})

	modifiedContent, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
	if err != nil {
		t.Fatalf("failed to read modified Chart.yaml: %v", err)
	}

	content := string(modifiedContent)

	requiredFields := []string{
		"apiVersion: v2",
		"name: test-chart",
		"version: 1.0.0",
		"description: A test chart",
		"keywords:",
		"maintainers:",
		"condition:",
	}

	for _, field := range requiredFields {
		if !strings.Contains(content, field) {
			t.Errorf("field %q should be preserved", field)
		}
	}

	if strings.Contains(content, "file://../relative-chart") {
		t.Errorf("relative path should have been converted to absolute")
	}

	originalContent, err := os.ReadFile(chartYamlPath)
	if err != nil {
		t.Fatalf("failed to read original Chart.yaml: %v", err)
	}
	if string(originalContent) != chartYaml {
		t.Errorf("original Chart.yaml should not have been modified")
	}
}

func TestRewriteChartDependencies_ErrorHandling(t *testing.T) {
	tests := []struct {
		name        string
		setupFunc   func(tempDir string) error
		expectError bool
	}{
		{
			name: "invalid yaml in Chart.yaml",
			setupFunc: func(tempDir string) error {
				invalidYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
  - name: dep1
    repository: file://../chart
    version: 1.0.0
    invalid yaml here!
`
				return os.WriteFile(filepath.Join(tempDir, "Chart.yaml"), []byte(invalidYaml), 0644)
			},
			expectError: true,
		},
		{
			name: "unreadable Chart.yaml",
			setupFunc: func(tempDir string) error {
				chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
				if err := os.WriteFile(chartYamlPath, []byte("test"), 0000); err != nil {
					return err
				}
				return nil
			},
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tempDir, err := os.MkdirTemp("", "helmfile-test-")
			if err != nil {
				t.Fatalf("failed to create temp dir: %v", err)
			}
			defer os.RemoveAll(tempDir)

			if err := tt.setupFunc(tempDir); err != nil {
				t.Fatalf("setup failed: %v", err)
			}

			logger := zap.NewNop().Sugar()
			st := &HelmState{
				logger: logger,
				fs:     filesystem.DefaultFileSystem(),
			}

			_, _, err = st.rewriteChartDependencies(tempDir)

			if tt.expectError && err == nil {
				t.Errorf("expected error but got none")
			}
			if !tt.expectError && err != nil {
				t.Errorf("unexpected error: %v", err)
			}
		})
	}
}

func TestRewriteChartDependencies_WindowsStylePath(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "helmfile-test-")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	chartYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
  - name: dep1
    repository: file://./subdir/chart
    version: 1.0.0
`

	chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
	if err := os.WriteFile(chartYamlPath, []byte(chartYaml), 0644); err != nil {
		t.Fatalf("failed to write Chart.yaml: %v", err)
	}

	logger := zap.NewNop().Sugar()
	st := &HelmState{
		logger: logger,
		fs:     filesystem.DefaultFileSystem(),
	}

	rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	t.Cleanup(func() {
		if rewrittenPath == tempDir {
			return
		}

		cleanup()

		if _, statErr := os.Stat(rewrittenPath); !os.IsNotExist(statErr) {
			t.Errorf("expected rewritten chart path %q to be removed after cleanup", rewrittenPath)
		}
	})

	data, err := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
	if err != nil {
		t.Fatalf("failed to read Chart.yaml: %v", err)
	}

	content := string(data)
	if strings.Contains(content, "file://./subdir/chart") {
		t.Errorf("relative path with ./ should have been converted")
	}
}

func TestRewriteChartDependencies_RaceCondition(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "helmfile-test-")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	chartYaml := `apiVersion: v2
name: test-chart
version: 1.0.0
dependencies:
  - name: dep1
    repository: file://../relative-chart
    version: 1.0.0
`

	chartYamlPath := filepath.Join(tempDir, "Chart.yaml")
	if err := os.WriteFile(chartYamlPath, []byte(chartYaml), 0644); err != nil {
		t.Fatalf("failed to write Chart.yaml: %v", err)
	}

	numGoroutines := 10
	var wg sync.WaitGroup
	var readyWg sync.WaitGroup
	errCh := make(chan error, numGoroutines)
	ready := make(chan struct{})

	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		readyWg.Add(1)
		go func() {
			defer wg.Done()

			readyWg.Done()
			<-ready

			logger := zap.NewNop().Sugar()
			st := &HelmState{
				logger: logger,
				fs:     filesystem.DefaultFileSystem(),
			}

			rewrittenPath, cleanup, err := st.rewriteChartDependencies(tempDir)
			if err != nil {
				errCh <- err
				return
			}
			defer cleanup()

			data, readErr := os.ReadFile(filepath.Join(rewrittenPath, "Chart.yaml"))
			if readErr != nil {
				errCh <- readErr
				return
			}

			type ChartDependency struct {
				Name       string `yaml:"name"`
				Repository string `yaml:"repository"`
				Version    string `yaml:"version"`
			}
			type ChartMeta struct {
				APIVersion   string            `yaml:"apiVersion"`
				Name         string            `yaml:"name"`
				Version      string            `yaml:"version"`
				Dependencies []ChartDependency `yaml:"dependencies,omitempty"`
			}

			var meta ChartMeta
			if unmarshalErr := yaml.Unmarshal(data, &meta); unmarshalErr != nil {
				errCh <- unmarshalErr
				return
			}

			if meta.Name != "test-chart" {
				errCh <- fmt.Errorf("expected chart name 'test-chart', got %q", meta.Name)
				return
			}
		}()
	}

	readyWg.Wait()
	close(ready)

	wg.Wait()
	close(errCh)

	for err := range errCh {
		t.Errorf("goroutine error: %v", err)
	}

	data, err := os.ReadFile(chartYamlPath)
	if err != nil {
		t.Fatalf("failed to read original Chart.yaml: %v", err)
	}

	type ChartDependency struct {
		Name       string `yaml:"name"`
		Repository string `yaml:"repository"`
		Version    string `yaml:"version"`
	}
	type ChartMeta struct {
		APIVersion   string            `yaml:"apiVersion"`
		Name         string            `yaml:"name"`
		Version      string            `yaml:"version"`
		Dependencies []ChartDependency `yaml:"dependencies,omitempty"`
	}

	var chartMeta ChartMeta
	if err := yaml.Unmarshal(data, &chartMeta); err != nil {
		t.Fatalf("original Chart.yaml is not valid YAML: %v", err)
	}

	if chartMeta.Name != "test-chart" {
		t.Errorf("expected original chart name 'test-chart', got %q", chartMeta.Name)
	}
	if len(chartMeta.Dependencies) == 0 {
		t.Fatalf("expected original Chart.yaml to contain at least one dependency, got %d", len(chartMeta.Dependencies))
	}

	const wantDependencyName = "dep1"
	if chartMeta.Dependencies[0].Name != wantDependencyName {
		t.Errorf("expected first dependency name %q, got %q", wantDependencyName, chartMeta.Dependencies[0].Name)
	}
	const wantRepository = "file://../relative-chart"
	if chartMeta.Dependencies[0].Repository != wantRepository {
		t.Errorf("expected original dependency repository %q, got %q", wantRepository, chartMeta.Dependencies[0].Repository)
	}
}
