//go:build functional
// +build functional

package functional

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"code.gitea.io/sdk/gitea"
)

// TestEnv holds the functional test environment
type TestEnv struct {
	Token    string
	Hostname string
	Owner    string
	RepoName string
	Client   *gitea.Client
	T        *testing.T
}

// NewTestEnv creates a new functional test environment from environment variables
func NewTestEnv(t *testing.T) *TestEnv {
	token := os.Getenv("CODEBERG_TEST_TOKEN")
	if token == "" {
		t.Skip("CODEBERG_TEST_TOKEN not set, skipping functional tests")
	}

	hostname := os.Getenv("CODEBERG_TEST_HOSTNAME")
	if hostname == "" {
		hostname = "codeberg.org"
	}

	owner := os.Getenv("CODEBERG_TEST_OWNER")
	if owner == "" {
		t.Fatal("CODEBERG_TEST_OWNER environment variable not set")
	}

	repoName := os.Getenv("CODEBERG_TEST_REPO")
	if repoName == "" {
		t.Fatal("CODEBERG_TEST_REPO environment variable not set")
	}

	// Create Gitea client
	client, err := gitea.NewClient(
		fmt.Sprintf("https://%s", hostname),
		gitea.SetToken(token),
	)
	if err != nil {
		t.Fatalf("failed to create Gitea client: %v", err)
	}

	env := &TestEnv{
		Token:    token,
		Hostname: hostname,
		Owner:    owner,
		RepoName: repoName,
		Client:   client,
		T:        t,
	}

	// Verify authentication by getting user info
	user, _, err := client.GetMyUserInfo()
	if err != nil {
		t.Fatalf("authentication failed: %v", err)
	}

	t.Logf("Authenticated as user: %s on %s", user.UserName, hostname)
	return env
}

// CreateTestIssue creates a test issue and returns its number
func (env *TestEnv) CreateTestIssue(title, body string) int64 {
	opts := gitea.CreateIssueOption{
		Title: title,
		Body:  body,
	}

	issue, _, err := env.Client.CreateIssue(env.Owner, env.RepoName, opts)
	if err != nil {
		env.T.Fatalf("failed to create test issue: %v", err)
	}

	env.T.Logf("Created test issue #%d", issue.Index)
	return issue.Index
}

// CleanupIssue closes and deletes a test issue
func (env *TestEnv) CleanupIssue(issueNumber int64) {
	closeState := gitea.StateClosed
	_, _, err := env.Client.EditIssue(env.Owner, env.RepoName, issueNumber, gitea.EditIssueOption{
		State: &closeState,
	})
	if err != nil {
		env.T.Logf("warning: failed to close issue #%d: %v", issueNumber, err)
	}
}

// CreateTestPullRequest creates a test PR (requires a branch to exist)
// Note: Creating PRs requires multiple branches, so this is a placeholder for future use
func (env *TestEnv) CreateTestPullRequest(title, body, head, base string) int64 {
	opts := gitea.CreatePullRequestOption{
		Head:  head,
		Base:  base,
		Title: title,
		Body:  body,
	}

	pr, _, err := env.Client.CreatePullRequest(env.Owner, env.RepoName, opts)
	if err != nil {
		env.T.Fatalf("failed to create test PR: %v", err)
	}

	env.T.Logf("Created test PR #%d", pr.Index)
	return pr.Index
}

// ListPullRequests gets all PRs in the repository
func (env *TestEnv) ListPullRequests() ([]*gitea.PullRequest, error) {
	prs, _, err := env.Client.ListRepoPullRequests(env.Owner, env.RepoName, gitea.ListPullRequestsOptions{})
	return prs, err
}

// GetPullRequest gets a specific PR
func (env *TestEnv) GetPullRequest(prNumber int64) (*gitea.PullRequest, error) {
	pr, _, err := env.Client.GetPullRequest(env.Owner, env.RepoName, prNumber)
	return pr, err
}

// CleanupPullRequest closes a test PR
func (env *TestEnv) CleanupPullRequest(prNumber int64) {
	closed := gitea.StateClosed
	_, _, err := env.Client.EditPullRequest(env.Owner, env.RepoName, prNumber, gitea.EditPullRequestOption{
		State: &closed,
	})
	if err != nil {
		env.T.Logf("warning: failed to close PR #%d: %v", prNumber, err)
	}
}

// VerifyAPIConnection verifies the test environment can connect to the API
func (env *TestEnv) VerifyAPIConnection() {
	_, _, err := env.Client.GetMyUserInfo()
	if err != nil {
		env.T.Fatalf("failed to verify API connection: %v", err)
	}
}

// EnsureTestLabels creates test labels if they don't exist
func (env *TestEnv) EnsureTestLabels() {
	labelNames := []string{"bug", "enhancement", "help-wanted"}

	for _, name := range labelNames {
		labels, _, err := env.Client.ListRepoLabels(env.Owner, env.RepoName, gitea.ListLabelsOptions{})
		if err != nil {
			env.T.Fatalf("failed to list labels: %v", err)
		}

		exists := false
		for _, label := range labels {
			if label.Name == name {
				exists = true
				break
			}
		}

		if !exists {
			color := "#00aabb"
			if name == "bug" {
				color = "#ff0000"
			} else if name == "enhancement" {
				color = "#00ff00"
			}
			_, _, err = env.Client.CreateLabel(env.Owner, env.RepoName, gitea.CreateLabelOption{
				Name:  name,
				Color: color,
			})
			if err != nil {
				env.T.Logf("warning: failed to create label '%s': %v", name, err)
			} else {
				env.T.Logf("Created test label: %s", name)
			}
		}
	}
}

// GetIssueLabels gets the labels on an issue
func (env *TestEnv) GetIssueLabels(issueNumber int64) ([]*gitea.Label, error) {
	labels, _, err := env.Client.GetIssueLabels(env.Owner, env.RepoName, issueNumber, gitea.ListLabelsOptions{})
	return labels, err
}

// GetLabelIDs converts label names to label IDs
func (env *TestEnv) GetLabelIDs(labelNames []string) []int64 {
	labels, _, err := env.Client.ListRepoLabels(env.Owner, env.RepoName, gitea.ListLabelsOptions{})
	if err != nil {
		env.T.Fatalf("failed to list labels: %v", err)
	}

	nameToID := make(map[string]int64)
	for _, label := range labels {
		nameToID[label.Name] = label.ID
	}

	var ids []int64
	for _, name := range labelNames {
		id, exists := nameToID[name]
		if !exists {
			env.T.Fatalf("label '%s' not found", name)
		}
		ids = append(ids, id)
	}

	return ids
}

// CleanupRepo deletes a repository created during testing.
func (env *TestEnv) CleanupRepo(owner, repoName string) {
	_, err := env.Client.DeleteRepo(owner, repoName)
	if err != nil {
		env.T.Logf("warning: failed to delete repo %s/%s: %v", owner, repoName, err)
	}
}

// GetBinaryPath returns the path to the built fj binary
func (env *TestEnv) GetBinaryPath() string {
	binaryPath := os.Getenv("FGJ_BINARY_PATH")
	if binaryPath == "" {
		// Look for the binary in common locations
		candidates := []string{
			"./bin/fj",
			"bin/fj",
			"/home/romain/work/fj/bin/fj",
		}
		for _, candidate := range candidates {
			if _, err := os.Stat(candidate); err == nil {
				// Convert to absolute path
				if abs, err := filepath.Abs(candidate); err == nil {
					return abs
				}
				return candidate
			}
		}
		// If no binary found, return default (will error when executed)
		binaryPath = "./bin/fj"
	}
	return binaryPath
}

// CLIResult holds the result of a CLI command execution
type CLIResult struct {
	Stdout   string
	Stderr   string
	ExitCode int
}

// RunCLI executes a CLI command and returns stdout, stderr, and exit code
func (env *TestEnv) RunCLI(args ...string) *CLIResult {
	cmd := exec.Command(env.GetBinaryPath(), args...)

	// Set up environment with test credentials
	cmd.Env = os.Environ()

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	exitCode := 0
	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			exitCode = exitErr.ExitCode()
		}
	}

	result := &CLIResult{
		Stdout:   stdout.String(),
		Stderr:   stderr.String(),
		ExitCode: exitCode,
	}

	env.T.Logf("Command: %s %v", env.GetBinaryPath(), args)
	env.T.Logf("Exit code: %d", exitCode)
	if stdout.Len() > 0 {
		env.T.Logf("Stdout:\n%s", result.Stdout)
	}
	if stderr.Len() > 0 {
		env.T.Logf("Stderr:\n%s", result.Stderr)
	}

	return result
}

// runCLIWithStdin executes the CLI binary with the given args and pipes input to stdin.
func (env *TestEnv) runCLIWithStdin(input string, args ...string) *CLIResult {
	cmd := exec.Command(env.GetBinaryPath(), args...)
	cmd.Env = os.Environ()
	cmd.Stdin = strings.NewReader(input)

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	exitCode := 0
	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			exitCode = exitErr.ExitCode()
		}
	}

	result := &CLIResult{
		Stdout:   stdout.String(),
		Stderr:   stderr.String(),
		ExitCode: exitCode,
	}

	env.T.Logf("Command: %s %v", env.GetBinaryPath(), args)
	env.T.Logf("Exit code: %d", exitCode)
	if stdout.Len() > 0 {
		env.T.Logf("Stdout:\n%s", result.Stdout)
	}
	if stderr.Len() > 0 {
		env.T.Logf("Stderr:\n%s", result.Stderr)
	}

	return result
}
