package toolingplan

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestBuildDetectsGoVersionFromGoMod(t *testing.T) {
	repoRoot := t.TempDir()
	writePlanFile(t, repoRoot, "go.mod", "module example.com/demo\n\ngo 1.25.0\n")

	plan := Build(context.Background(), repoRoot)

	if len(plan.Steps) != 1 {
		t.Fatalf("steps = %#v, want one step", plan.Steps)
	}
	step := plan.Steps[0]
	if step.Tool != "go" || step.Version != "1.25.0" || step.Source != "go.mod" {
		t.Fatalf("step = %#v, want go@1.25.0 from go.mod", step)
	}
}

func TestBuildSkipsGoWhenRepoMiseAlreadyDeclaresIt(t *testing.T) {
	repoRoot := t.TempDir()
	writePlanFile(t, repoRoot, ".mise.toml", "[tools]\ngo = '1.25.0'\n")
	writePlanFile(t, repoRoot, "go.mod", "module example.com/demo\n\ngo 1.25.0\n")

	plan := Build(context.Background(), repoRoot)

	if len(plan.Steps) != 0 {
		t.Fatalf("steps = %#v, want no deterministic go install", plan.Steps)
	}
	if !containsSkip(plan.Skips, "go", "already managed by repo mise declarations") {
		t.Fatalf("skips = %#v, want managed go skip", plan.Skips)
	}
	if len(plan.RepoManagedTools) != 1 || plan.RepoManagedTools[0] != "go" {
		t.Fatalf("repo managed tools = %#v, want [go]", plan.RepoManagedTools)
	}
}

func TestBuildDetectsNodeAndPackageManager(t *testing.T) {
	repoRoot := t.TempDir()
	writePlanFile(t, repoRoot, ".node-version", "v22.14.0\n")
	writePlanFile(t, repoRoot, "package.json", `{"packageManager":"pnpm@9.15.2"}`)

	plan := Build(context.Background(), repoRoot)

	if !containsStep(plan.Steps, "node", "22.14.0", ".node-version") {
		t.Fatalf("steps = %#v, want node step", plan.Steps)
	}
	if !containsStep(plan.Steps, "pnpm", "9.15.2", "package.json#packageManager") {
		t.Fatalf("steps = %#v, want pnpm step", plan.Steps)
	}
}

func TestBuildSkipsPackageManagerWhenNodeIsNotPinned(t *testing.T) {
	repoRoot := t.TempDir()
	writePlanFile(t, repoRoot, "package.json", `{"packageManager":"pnpm@9.15.2"}`)

	plan := Build(context.Background(), repoRoot)

	if containsStep(plan.Steps, "pnpm", "9.15.2", "package.json#packageManager") {
		t.Fatalf("steps = %#v, want no package manager install", plan.Steps)
	}
	if !containsSkip(plan.Skips, "node package manager", "packageManager is pinned but node is not pinned") {
		t.Fatalf("skips = %#v, want node package manager skip", plan.Skips)
	}
}

func TestBuildDetectsPythonAndRust(t *testing.T) {
	repoRoot := t.TempDir()
	writePlanFile(t, repoRoot, ".python-version", "3.12.9\n")
	writePlanFile(t, repoRoot, "rust-toolchain.toml", "[toolchain]\nchannel = '1.86.0'\n")

	plan := Build(context.Background(), repoRoot)

	if !containsStep(plan.Steps, "python", "3.12.9", ".python-version") {
		t.Fatalf("steps = %#v, want python step", plan.Steps)
	}
	if !containsStep(plan.Steps, "rust", "1.86.0", "rust-toolchain.toml") {
		t.Fatalf("steps = %#v, want rust step", plan.Steps)
	}
}

func TestBuildSkipsRustChannelNames(t *testing.T) {
	repoRoot := t.TempDir()
	writePlanFile(t, repoRoot, "rust-toolchain.toml", "[toolchain]\nchannel = 'stable'\n")

	plan := Build(context.Background(), repoRoot)

	if !containsSkip(plan.Skips, "rust", "rust-toolchain.toml channel is not an exact version") {
		t.Fatalf("skips = %#v, want rust exact-version skip", plan.Skips)
	}
}

func TestBuildReportsMalformedMiseTomlAsSkip(t *testing.T) {
	repoRoot := t.TempDir()
	writePlanFile(t, repoRoot, ".mise.toml", "[tools\nbroken")

	plan := Build(context.Background(), repoRoot)

	if !containsSkip(plan.Skips, "repo mise declarations", "could not parse .mise.toml") {
		t.Fatalf("skips = %#v, want malformed .mise.toml skip", plan.Skips)
	}
}

func writePlanFile(t *testing.T, repoRoot, relativePath, contents string) {
	t.Helper()
	path := filepath.Join(repoRoot, relativePath)
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("MkdirAll(%s): %v", filepath.Dir(path), err)
	}
	if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
		t.Fatalf("WriteFile(%s): %v", relativePath, err)
	}
}

func containsStep(steps []InstallStep, tool, version, source string) bool {
	for _, step := range steps {
		if step.Tool == tool && step.Version == version && step.Source == source {
			return true
		}
	}
	return false
}

func containsSkip(skips []SkipNote, target, reasonContains string) bool {
	for _, skip := range skips {
		if skip.Target == target && strings.Contains(skip.Reason, reasonContains) {
			return true
		}
	}
	return false
}
