package exec

import (
	"testing"

	cfg "github.com/cloudposse/atmos/pkg/config"
	"github.com/cloudposse/atmos/pkg/schema"
	"github.com/cloudposse/atmos/pkg/version"
	"github.com/golang/mock/gomock"
	"github.com/pkg/errors"
	"github.com/stretchr/testify/assert"
)

// Mock interfaces generated by mockgen
// VersionExecutor defines the interface for version execution operations
//
//go:generate mockgen -source=$GOFILE -destination=mock_$GOFILE -package=$GOPACKAGE
type VersionExecutor interface {
	PrintStyledText(text string) error
	GetLatestGitHubRepoRelease() (string, error)
	PrintMessage(message string)
	PrintMessageToUpgradeToAtmosLatestRelease(version string)
}

func TestVersionExec_Execute(t *testing.T) {
	// Save original values
	originalVersion := version.Version
	defer func() { version.Version = originalVersion }()

	tests := []struct {
		name                string
		checkFlag           bool
		version             string
		latestRelease       string
		printStyledTextErr  error
		getLatestReleaseErr error
		format              string
	}{
		{
			name:      "Basic execution without check",
			checkFlag: false,
			version:   "v1.0.0",
		},
		{
			name:          "Check flag with same version",
			checkFlag:     true,
			version:       "v1.0.0",
			latestRelease: "v1.0.0",
		},
		{
			name:          "Check flag with different version",
			checkFlag:     true,
			version:       "v1.0.0",
			latestRelease: "v1.1.0",
		},
		{
			name:                "Check flag with release check error",
			checkFlag:           true,
			version:             "v1.0.0",
			getLatestReleaseErr: errors.New("github error"),
		},
		{
			name:      "Check format json works",
			format:    "json",
			checkFlag: false,
			version:   "v1.0.0",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Setup mock controller
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			// Create mocks
			mockExec := NewMockVersionExecutor(ctrl)

			// Set version
			version.Version = tt.version

			// Setup mock expectations
			mockExec.EXPECT().PrintStyledText("ATMOS").Return(tt.printStyledTextErr)
			//nolint:nestif
			if tt.printStyledTextErr == nil {
				mockExec.EXPECT().PrintMessage(gomock.Any()).Times(3)

				if tt.checkFlag {
					mockExec.EXPECT().GetLatestGitHubRepoRelease().
						Return(tt.latestRelease, tt.getLatestReleaseErr)

					if tt.getLatestReleaseErr == nil && tt.latestRelease != "" {
						if tt.latestRelease != tt.version {
							mockExec.EXPECT().PrintMessageToUpgradeToAtmosLatestRelease(
								gomock.Eq(tt.latestRelease[1:])).Times(1)
						}
					}
				}
			}

			// Create test instance with mocks
			v := versionExec{
				atmosConfig:                &schema.AtmosConfiguration{},
				printStyledText:            mockExec.PrintStyledText,
				printMessage:               mockExec.PrintMessage,
				getLatestGitHubRepoRelease: mockExec.GetLatestGitHubRepoRelease,
				printMessageToUpgradeToAtmosLatestRelease: mockExec.PrintMessageToUpgradeToAtmosLatestRelease,
			}

			// Execute the function
			v.Execute(tt.checkFlag, "")
		})
	}
}

func TestIsCheckVersionEnabled(t *testing.T) {
	tests := []struct {
		name                  string
		config                *schema.AtmosConfiguration
		expected              bool
		forceCheck            bool
		loadCacheConfig       func() (cfg.CacheConfig, error)
		shouldCheckForUpdates func(lastChecked int64, frequency string) bool
	}{
		{
			name: "Check version enabled",
			config: &schema.AtmosConfiguration{
				Version: schema.Version{
					Check: schema.VersionCheck{
						Enabled: true,
					},
				},
			},
			loadCacheConfig: func() (cfg.CacheConfig, error) {
				return cfg.CacheConfig{LastChecked: 0}, nil
			},
			shouldCheckForUpdates: func(lastChecked int64, frequency string) bool {
				return true
			},
			expected: true,
		},
		{
			name: "Check version disabled",
			config: &schema.AtmosConfiguration{
				Version: schema.Version{
					Check: schema.VersionCheck{
						Enabled: false,
					},
				},
			},
			expected: false,
		},
		{
			name: "Force check version enabled",
			config: &schema.AtmosConfiguration{
				Version: schema.Version{
					Check: schema.VersionCheck{
						Enabled: false,
					},
				},
			},
			expected:   true,
			forceCheck: true,
		},
		{
			name: "Check version enabled with cache",
			config: &schema.AtmosConfiguration{
				Version: schema.Version{
					Check: schema.VersionCheck{
						Enabled: true,
					},
				},
			},
			loadCacheConfig: func() (cfg.CacheConfig, error) {
				return cfg.CacheConfig{LastChecked: 0}, nil
			},
			shouldCheckForUpdates: func(lastChecked int64, frequency string) bool {
				return true
			},
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			v := versionExec{
				atmosConfig:           tt.config,
				loadCacheConfig:       tt.loadCacheConfig,
				shouldCheckForUpdates: tt.shouldCheckForUpdates,
			}
			result := v.isCheckVersionEnabled(tt.forceCheck)
			if result != tt.expected {
				t.Errorf("expected %v, got %v", tt.expected, result)
			}
		})
	}
}

func TestGetLatestVersion(t *testing.T) {
	tests := []struct {
		name                string
		version             string
		latestRelease       string
		getLatestReleaseErr error
		expectedVersion     string
		expectedOk          bool
	}{
		{
			name:            "Same version",
			version:         "v1.0.0",
			latestRelease:   "v1.0.0",
			expectedVersion: "",
			expectedOk:      false,
		},
		{
			name:            "Newer version available",
			version:         "v1.0.0",
			latestRelease:   "v1.1.0",
			expectedVersion: "1.1.0",
			expectedOk:      true,
		},
		{
			name:                "Error fetching latest release",
			version:             "v1.0.0",
			getLatestReleaseErr: errors.New("error"),
			expectedVersion:     "",
			expectedOk:          false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			oldVersion := version.Version
			version.Version = tt.version
			t.Cleanup(func() {
				version.Version = oldVersion // Reset to a known state
			})
			defer ctrl.Finish()

			mockExec := NewMockVersionExecutor(ctrl)
			mockExec.EXPECT().GetLatestGitHubRepoRelease().
				Return(tt.latestRelease, tt.getLatestReleaseErr).AnyTimes()

			v := versionExec{
				atmosConfig:                &schema.AtmosConfiguration{},
				getLatestGitHubRepoRelease: mockExec.GetLatestGitHubRepoRelease,
			}

			version, ok := v.GetLatestVersion(true)
			assert.Equal(t, tt.expectedVersion, version, "Expected version mismatch")
			assert.Equal(t, tt.expectedOk, ok, "Expected ok mismatch")
		})
	}
}

func TestDisplayVersionInFormat(t *testing.T) {
	tests := []struct {
		name          string
		format        string
		expectedError error
	}{
		{
			name:   "Valid JSON format",
			format: "json",
		},
		{
			name:   "Valid YAML format",
			format: "yaml",
		},
		{
			name:          "Invalid format",
			format:        "xml",
			expectedError: ErrInvalidFormat,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctrl := gomock.NewController(t)
			defer ctrl.Finish()

			mockExec := NewMockVersionExecutor(ctrl)

			v := versionExec{
				atmosConfig:                &schema.AtmosConfiguration{},
				printStyledText:            mockExec.PrintStyledText,
				printMessage:               mockExec.PrintMessage,
				getLatestGitHubRepoRelease: mockExec.GetLatestGitHubRepoRelease,
				printMessageToUpgradeToAtmosLatestRelease: mockExec.PrintMessageToUpgradeToAtmosLatestRelease,
			}

			err := v.displayVersionInFormat(false, tt.format)
			if tt.expectedError != nil {
				assert.Equal(t, tt.expectedError, err, "Expected error mismatch")
			} else {
				assert.NoError(t, err, "Expected no error")
			}
		})
	}
}
