// Package utils provides utility functions for AutoSSO.
// This file contains tests to verify the project directory structure and naming conventions.
package utils

import (
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"
)

// TestRequiredDirectoriesExist verifies that all required directories exist in the project structure.
// Validates: Requirements 1.1, 1.2
func TestRequiredDirectoriesExist(t *testing.T) {
	// Get the project root directory (3 levels up from internal/pkg/utils)
	projectRoot, err := getProjectRoot()
	if err != nil {
		t.Fatalf("Failed to get project root: %v", err)
	}

	requiredDirs := []string{
		"cmd/autosso",
		"internal/config",
		"internal/provider",
		"internal/feishu",
		"internal/state",
		"internal/permission",
		"internal/pkg",
		"internal/pkg/utils",
		"internal/pkg/logger",
	}

	for _, dir := range requiredDirs {
		dirPath := filepath.Join(projectRoot, dir)
		info, err := os.Stat(dirPath)
		if os.IsNotExist(err) {
			t.Errorf("Required directory does not exist: %s", dir)
			continue
		}
		if err != nil {
			t.Errorf("Error checking directory %s: %v", dir, err)
			continue
		}
		if !info.IsDir() {
			t.Errorf("Path exists but is not a directory: %s", dir)
		}
	}
}

// TestFileNamesFollowSnakeCase verifies that all Go file names follow snake_case convention.
// Validates: Requirement 1.4 - All file names should be lowercase snake_case
func TestFileNamesFollowSnakeCase(t *testing.T) {
	projectRoot, err := getProjectRoot()
	if err != nil {
		t.Fatalf("Failed to get project root: %v", err)
	}

	// snake_case pattern: lowercase letters, numbers, and underscores only
	// Must start with a letter, can contain underscores between words
	snakeCasePattern := regexp.MustCompile(`^[a-z][a-z0-9]*(_[a-z0-9]+)*\.go$`)

	// Also allow test files with _test suffix
	testFilePattern := regexp.MustCompile(`^[a-z][a-z0-9]*(_[a-z0-9]+)*_test\.go$`)

	// Directories to check for Go files
	dirsToCheck := []string{
		"cmd/autosso",
		"internal/config",
		"internal/provider",
		"internal/feishu",
		"internal/state",
		"internal/permission",
		"internal/pkg/utils",
		"internal/pkg/logger",
		"awssso",
		"gcpsso",
		"feishu",
	}

	var violations []string

	for _, dir := range dirsToCheck {
		dirPath := filepath.Join(projectRoot, dir)
		if _, err := os.Stat(dirPath); os.IsNotExist(err) {
			// Skip directories that don't exist yet
			continue
		}

		err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}

			// Skip directories
			if info.IsDir() {
				return nil
			}

			// Only check .go files
			if !strings.HasSuffix(info.Name(), ".go") {
				return nil
			}

			fileName := info.Name()

			// Check if file name matches snake_case pattern
			if !snakeCasePattern.MatchString(fileName) && !testFilePattern.MatchString(fileName) {
				relPath, _ := filepath.Rel(projectRoot, path)
				violations = append(violations, relPath)
			}

			return nil
		})

		if err != nil {
			t.Errorf("Error walking directory %s: %v", dir, err)
		}
	}

	if len(violations) > 0 {
		t.Errorf("The following files do not follow snake_case naming convention:\n%s",
			strings.Join(violations, "\n"))
	}
}

// TestPackageNamesAreLowercase verifies that all package names are lowercase without underscores.
// Validates: Requirement 1.7 - All package names should be lowercase without underscores
func TestPackageNamesAreLowercase(t *testing.T) {
	projectRoot, err := getProjectRoot()
	if err != nil {
		t.Fatalf("Failed to get project root: %v", err)
	}

	// Package name pattern: lowercase letters and numbers only, no underscores
	packageNamePattern := regexp.MustCompile(`^[a-z][a-z0-9]*$`)

	// Directories to check for Go packages
	dirsToCheck := []string{
		"cmd/autosso",
		"internal/config",
		"internal/provider",
		"internal/feishu",
		"internal/state",
		"internal/permission",
		"internal/pkg/utils",
		"internal/pkg/logger",
		"awssso",
		"gcpsso",
		"feishu",
	}

	var violations []string
	checkedPackages := make(map[string]bool)

	fset := token.NewFileSet()

	for _, dir := range dirsToCheck {
		dirPath := filepath.Join(projectRoot, dir)
		if _, err := os.Stat(dirPath); os.IsNotExist(err) {
			// Skip directories that don't exist yet
			continue
		}

		err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}

			// Skip directories
			if info.IsDir() {
				return nil
			}

			// Only check .go files
			if !strings.HasSuffix(info.Name(), ".go") {
				return nil
			}

			// Parse the Go file to get the package name
			file, err := parser.ParseFile(fset, path, nil, parser.PackageClauseOnly)
			if err != nil {
				// Skip files that can't be parsed
				return nil
			}

			packageName := file.Name.Name

			// Skip if we've already checked this package
			packageKey := filepath.Dir(path) + ":" + packageName
			if checkedPackages[packageKey] {
				return nil
			}
			checkedPackages[packageKey] = true

			// Check if package name matches the pattern (lowercase, no underscores)
			if !packageNamePattern.MatchString(packageName) {
				relPath, _ := filepath.Rel(projectRoot, filepath.Dir(path))
				violations = append(violations, relPath+": package "+packageName)
			}

			return nil
		})

		if err != nil {
			t.Errorf("Error walking directory %s: %v", dir, err)
		}
	}

	if len(violations) > 0 {
		t.Errorf("The following packages do not follow lowercase naming convention (no underscores):\n%s",
			strings.Join(violations, "\n"))
	}
}

// TestMainGoExists verifies that main.go exists in cmd/autosso directory.
// Validates: Requirement 1.3 - mains.go should be renamed to main.go
func TestMainGoExists(t *testing.T) {
	projectRoot, err := getProjectRoot()
	if err != nil {
		t.Fatalf("Failed to get project root: %v", err)
	}

	mainGoPath := filepath.Join(projectRoot, "cmd", "autosso", "main.go")
	if _, err := os.Stat(mainGoPath); os.IsNotExist(err) {
		t.Error("main.go does not exist in cmd/autosso/")
	}
}

// TestNoUppercaseDirectories verifies that there are no uppercase directory names in internal/.
// Validates: Requirement 1.1 - Pbs/ should be renamed to lowercase
func TestNoUppercaseDirectories(t *testing.T) {
	projectRoot, err := getProjectRoot()
	if err != nil {
		t.Fatalf("Failed to get project root: %v", err)
	}

	internalPath := filepath.Join(projectRoot, "internal")
	if _, err := os.Stat(internalPath); os.IsNotExist(err) {
		t.Skip("internal/ directory does not exist yet")
		return
	}

	var violations []string

	err = filepath.Walk(internalPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if !info.IsDir() {
			return nil
		}

		// Skip testdata directories - these are auto-generated by testing frameworks
		// (e.g., rapid creates directories based on test function names which may contain uppercase)
		if strings.Contains(path, "testdata") {
			return filepath.SkipDir
		}

		dirName := info.Name()
		// Check if directory name contains uppercase letters
		if dirName != strings.ToLower(dirName) {
			relPath, _ := filepath.Rel(projectRoot, path)
			violations = append(violations, relPath)
		}

		return nil
	})

	if err != nil {
		t.Errorf("Error walking internal directory: %v", err)
	}

	if len(violations) > 0 {
		t.Errorf("The following directories contain uppercase letters:\n%s",
			strings.Join(violations, "\n"))
	}
}

// TestSpecificFileRenames verifies that specific file renames have been completed.
// Validates: Requirements 1.5, 1.6
func TestSpecificFileRenames(t *testing.T) {
	projectRoot, err := getProjectRoot()
	if err != nil {
		t.Fatalf("Failed to get project root: %v", err)
	}

	// File rename mappings: old name -> new name
	// We verify that the new name exists with correct casing
	renamedFiles := []struct {
		dir     string
		oldName string
		newName string
	}{
		{"feishu", "Alibaba_Permisions_Manage.go", "alibaba_permissions_manage.go"}, // Misspelled -> corrected
		{"feishu", "AddPermission.go", "add_permission.go"},
		{"feishu", "GetProgressInfo.go", "progress_info.go"},
		{"feishu", "Notice.go", "notice.go"},
		{"feishu", "RemovePermission.go", "remove_permission.go"},
		{"awssso", "AWSconfig.go", "aws_config.go"},
	}

	// Check that new files exist with correct casing
	for _, rf := range renamedFiles {
		dirPath := filepath.Join(projectRoot, rf.dir)
		if _, err := os.Stat(dirPath); os.IsNotExist(err) {
			continue // Skip if directory doesn't exist
		}

		// Read directory entries to check actual file names (case-sensitive check)
		entries, err := os.ReadDir(dirPath)
		if err != nil {
			t.Errorf("Error reading directory %s: %v", rf.dir, err)
			continue
		}

		foundNew := false
		foundOld := false
		for _, entry := range entries {
			if entry.Name() == rf.newName {
				foundNew = true
			}
			if entry.Name() == rf.oldName {
				foundOld = true
			}
		}

		if foundOld {
			t.Errorf("Old file still exists with incorrect name: %s/%s (should be %s)",
				rf.dir, rf.oldName, rf.newName)
		}

		if !foundNew {
			t.Errorf("Expected file does not exist: %s/%s", rf.dir, rf.newName)
		}
	}

	// Check that main.go exists in cmd/autosso (moved from cmds/mains.go)
	mainGoPath := filepath.Join(projectRoot, "cmd", "autosso", "main.go")
	if _, err := os.Stat(mainGoPath); os.IsNotExist(err) {
		t.Error("main.go does not exist in cmd/autosso/ (should be moved from cmds/mains.go)")
	}

	// Check that old cmds directory doesn't exist or is empty
	cmdsPath := filepath.Join(projectRoot, "cmds")
	if info, err := os.Stat(cmdsPath); err == nil && info.IsDir() {
		entries, _ := os.ReadDir(cmdsPath)
		for _, entry := range entries {
			if entry.Name() == "mains.go" {
				t.Error("Old file cmds/mains.go still exists (should be moved to cmd/autosso/main.go)")
			}
		}
	}
}

// getProjectRoot returns the absolute path to the project root directory.
// It navigates up from the current test file location.
func getProjectRoot() (string, error) {
	// Get the current working directory
	cwd, err := os.Getwd()
	if err != nil {
		return "", err
	}

	// Navigate up to find the project root (where go.mod is located)
	dir := cwd
	for {
		goModPath := filepath.Join(dir, "go.mod")
		if _, err := os.Stat(goModPath); err == nil {
			return dir, nil
		}

		parent := filepath.Dir(dir)
		if parent == dir {
			// Reached the root without finding go.mod
			break
		}
		dir = parent
	}

	// If we can't find go.mod, try to use relative path from test location
	// Assuming test is in internal/pkg/utils/
	return filepath.Abs(filepath.Join(cwd, "..", "..", ".."))
}

// IsSnakeCase checks if a string follows snake_case convention.
// Exported for potential use in other packages.
func IsSnakeCase(s string) bool {
	// Remove .go extension if present
	name := strings.TrimSuffix(s, ".go")
	name = strings.TrimSuffix(name, "_test")

	// snake_case: lowercase letters, numbers, and underscores
	// Must start with a letter
	pattern := regexp.MustCompile(`^[a-z][a-z0-9]*(_[a-z0-9]+)*$`)
	return pattern.MatchString(name)
}

// IsValidPackageName checks if a string is a valid Go package name.
// Package names should be lowercase without underscores.
// Exported for potential use in other packages.
func IsValidPackageName(name string) bool {
	pattern := regexp.MustCompile(`^[a-z][a-z0-9]*$`)
	return pattern.MatchString(name)
}

// TestIsSnakeCase verifies the IsSnakeCase helper function.
func TestIsSnakeCase(t *testing.T) {
	tests := []struct {
		input    string
		expected bool
	}{
		// Valid snake_case
		{"config.go", true},
		{"aws_config.go", true},
		{"alibaba_permissions_manage.go", true},
		{"progress_info.go", true},
		{"add_permission.go", true},
		{"my_long_file_name.go", true},
		{"file123.go", true},
		{"file_123.go", true},
		{"a.go", true},

		// Valid test files
		{"config_test.go", true},
		{"aws_config_test.go", true},

		// Invalid - uppercase letters
		{"Config.go", false},
		{"AWSconfig.go", false},
		{"AddPermission.go", false},
		{"GetProgressInfo.go", false},
		{"Notice.go", false},

		// Invalid - mixed case
		{"awsConfig.go", false},
		{"myFile.go", false},

		// Invalid - starts with number
		{"123file.go", false},

		// Invalid - consecutive underscores
		{"my__file.go", false},

		// Invalid - starts with underscore
		{"_config.go", false},

		// Invalid - ends with underscore before extension
		{"config_.go", false},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			result := IsSnakeCase(tt.input)
			if result != tt.expected {
				t.Errorf("IsSnakeCase(%q) = %v, expected %v", tt.input, result, tt.expected)
			}
		})
	}
}

// TestIsValidPackageName verifies the IsValidPackageName helper function.
func TestIsValidPackageName(t *testing.T) {
	tests := []struct {
		input    string
		expected bool
	}{
		// Valid package names
		{"config", true},
		{"provider", true},
		{"feishu", true},
		{"state", true},
		{"permission", true},
		{"utils", true},
		{"logger", true},
		{"awssso", true},
		{"gcpsso", true},
		{"main", true},
		{"pkg", true},
		{"http2", true},
		{"v2", true},

		// Invalid - contains underscores
		{"aws_sso", false},
		{"my_package", false},
		{"permission_manager", false},

		// Invalid - uppercase letters
		{"Config", false},
		{"AWS", false},
		{"MyPackage", false},
		{"Pbs", false},

		// Invalid - starts with number
		{"2http", false},
		{"123pkg", false},

		// Invalid - contains special characters
		{"my-package", false},
		{"my.package", false},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			result := IsValidPackageName(tt.input)
			if result != tt.expected {
				t.Errorf("IsValidPackageName(%q) = %v, expected %v", tt.input, result, tt.expected)
			}
		})
	}
}
