package codewiki

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestGenerateRunsOpenCodeAndWritesWikiToOutputRoot(t *testing.T) {
	inputRoot := t.TempDir()
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	writeFile(t, inputRoot, "main.go", "package main\n")
	writeFile(t, inputRoot, "README.md", "# Existing docs\n")
	writeFile(t, inputRoot, "node_modules/ignored/index.js", "ignored\n")

	var calls []string
	result, err := Generate(context.Background(), Options{
		InputRoot:      inputRoot,
		OutputRoot:     outputRoot,
		OpenCodeBinary: "custom-opencode",
		OpenCodeAgent:  "writer",
		OpenCodeModel:  "provider/model",
		TaskTimeout:    time.Minute,
		OpenCodeExtraArgs: []string{
			"--log-level",
			"INFO",
		},
	}, func(_ context.Context, command Command) (CommandResult, error) {
		require.Equal(t, "custom-opencode", command.Name)
		require.Equal(t, "run", command.Args[0])
		require.NotContains(t, command.Args, "--dangerously-skip-permissions")
		require.Contains(t, command.Args, "--agent")
		require.Contains(t, command.Args, "writer")
		require.Contains(t, command.Args, "--model")
		require.Contains(t, command.Args, "provider/model")
		require.Contains(t, command.Args, "--log-level")
		require.Contains(t, strings.Join(command.Args, "\n"), "code wiki")
		require.True(t, strings.HasPrefix(command.Dir, filepath.Join(outputRoot, ".code-wiki")+string(os.PathSeparator)))
		requireOpenCodeTaskDenied(t, command.Dir)

		if filepath.Base(command.Dir) == "planner" {
			require.Equal(t, command.Dir, argValue(command.Args, "--dir"))
			require.Empty(t, envValue(command.Env, "OPENCODE_CONFIG"))
			calls = append(calls, "planner")
			require.NoFileExists(t, filepath.Join(command.Dir, "wiki_plan_task.json"))
			require.Contains(t, strings.Join(command.Args, "\n"), ": "+inputRoot)
			require.Contains(t, strings.Join(command.Args, "\n"), "- "+outputRoot)
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Project Overview",
					Summary: "Entry point for the generated wiki.",
				},
				Sections: []WikiSection{
					{
						ID:      "architecture",
						Title:   "Architecture",
						Summary: "Runtime structure.",
						Pages: []WikiPage{
							{
								ID:      "architecture",
								Path:    "architecture.md",
								Title:   "Architecture",
								Summary: "Runtime structure.",
							},
						},
					},
				},
			})
			return CommandResult{Stdout: []byte("planned\n")}, nil
		}

		require.Equal(t, command.Dir, argValue(command.Args, "--dir"))
		require.NotEqual(t, outputRoot, argValue(command.Args, "--dir"))
		require.Empty(t, envValue(command.Env, "OPENCODE_CONFIG"))
		require.NoFileExists(t, filepath.Join(command.Dir, "page.md"))
		prompt := strings.Join(command.Args, "\n")
		switch {
		case strings.Contains(prompt, "- path: README.md"):
			calls = append(calls, "home")
			if err := writeWikiOutput(outputRoot, "README.md", "# Wiki\n"); err != nil {
				return CommandResult{}, err
			}
		case strings.Contains(prompt, "- path: architecture.md"):
			calls = append(calls, "architecture")
			if err := writeWikiOutput(outputRoot, "architecture.md", "# Architecture\n"); err != nil {
				return CommandResult{}, err
			}
		default:
			t.Fatalf("unexpected page prompt: %s", prompt)
		}
		return CommandResult{Stdout: []byte("done\n")}, nil
	})

	require.NoError(t, err)
	require.Equal(t, outputRoot, result.WikiDir)
	require.Equal(t, 3, result.TasksRun)
	require.Equal(t, []string{"planner", "home", "architecture"}, calls)
	require.FileExists(t, filepath.Join(outputRoot, "README.md"))
	require.FileExists(t, filepath.Join(outputRoot, "architecture.md"))
}

func TestGenerateUsesInputRootsWithoutFileDiscovery(t *testing.T) {
	inputRoot := filepath.Join(t.TempDir(), "empty-repo.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	require.NoError(t, os.MkdirAll(inputRoot, 0o755))

	var stderr bytes.Buffer
	result, err := Generate(context.Background(), Options{
		InputRoot:      inputRoot,
		OutputRoot:     outputRoot,
		ProgressWriter: &stderr,
	}, func(_ context.Context, command Command) (CommandResult, error) {
		if filepath.Base(command.Dir) == "planner" {
			require.Contains(t, strings.Join(command.Args, "\n"), "- empty-repo: "+inputRoot)
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Home",
					Summary: "Overview.",
				},
			})
			return CommandResult{}, nil
		}
		if err := writeTaskWikiOutput(command, outputRoot, "# Wiki\n"); err != nil {
			return CommandResult{}, err
		}
		return CommandResult{}, nil
	})

	require.NoError(t, err)
	require.Equal(t, outputRoot, result.WikiDir)
	require.Equal(t, 2, result.TasksRun)
	require.NotContains(t, stderr.String(), "discovering wiki input files")
	require.NotContains(t, stderr.String(), "discovered ")
	require.FileExists(t, filepath.Join(outputRoot, "README.md"))
}

func TestGenerateCombinesMultipleInputsInPlannerAndPagePrompts(t *testing.T) {
	inputRootA := filepath.Join(t.TempDir(), "repo-a.kb")
	inputRootB := filepath.Join(t.TempDir(), "repo-b.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	writeFile(t, inputRootA, "README.md", "# Repo A\n")
	writeFile(t, inputRootB, "docs/design.md", "# Repo B\n")

	result, err := Generate(context.Background(), Options{
		InputRoots: []string{inputRootA, inputRootB},
		OutputRoot: outputRoot,
	}, func(_ context.Context, command Command) (CommandResult, error) {
		if filepath.Base(command.Dir) == "planner" {
			require.NoFileExists(t, filepath.Join(command.Dir, "wiki_plan_task.json"))
			prompt := strings.Join(command.Args, "\n")
			require.Contains(t, prompt, "- repo-a: "+inputRootA)
			require.Contains(t, prompt, "- repo-b: "+inputRootB)
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Combined Overview",
					Summary: "Cross-repo overview.",
				},
			})
			return CommandResult{}, nil
		}

		require.NoFileExists(t, filepath.Join(command.Dir, "page.md"))
		prompt := strings.Join(command.Args, "\n")
		require.Contains(t, prompt, "- repo-a: "+inputRootA)
		require.Contains(t, prompt, "- repo-b: "+inputRootB)
		require.Contains(t, prompt, "Use the page title, summary, and path to identify relevant input directories")
		if err := writeTaskWikiOutput(command, outputRoot, "# Combined Wiki\n"); err != nil {
			return CommandResult{}, err
		}
		return CommandResult{}, nil
	})

	require.NoError(t, err)
	require.Equal(t, outputRoot, result.WikiDir)
	require.Equal(t, 2, result.TasksRun)
	require.FileExists(t, filepath.Join(outputRoot, "README.md"))
}

func TestGenerateRunsSecondLevelWikiPagesAsTasks(t *testing.T) {
	inputRoot := filepath.Join(t.TempDir(), "repo.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	writeFile(t, inputRoot, "README.md", "# Repo\n")
	writeFile(t, inputRoot, "server.go", "package main\n")

	var calls []string
	result, err := Generate(context.Background(), Options{
		InputRoot:  inputRoot,
		OutputRoot: outputRoot,
	}, func(_ context.Context, command Command) (CommandResult, error) {
		if filepath.Base(command.Dir) == "planner" {
			calls = append(calls, "planner")
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Home",
					Summary: "Overview.",
				},
				Sections: []WikiSection{
					{
						ID:      "architecture",
						Title:   "Architecture",
						Summary: "Runtime architecture.",
						Pages: []WikiPage{
							{
								ID:      "architecture-overview",
								Path:    "architecture/overview.md",
								Title:   "Architecture Overview",
								Summary: "System boundaries and major components.",
							},
							{
								ID:      "architecture-server",
								Path:    "architecture/server.md",
								Title:   "Server Runtime",
								Summary: "Server startup and request handling.",
							},
						},
					},
				},
			})
			return CommandResult{}, nil
		}

		prompt := strings.Join(command.Args, "\n")
		switch {
		case strings.Contains(prompt, "- path: README.md"):
			calls = append(calls, "home")
			if err := writeTaskWikiOutput(command, outputRoot, "# Wiki\n"); err != nil {
				return CommandResult{}, err
			}
		case strings.Contains(prompt, "- path: architecture/overview.md"):
			calls = append(calls, "architecture-overview")
			if err := writeTaskWikiOutput(command, outputRoot, "# Architecture Overview\n"); err != nil {
				return CommandResult{}, err
			}
		case strings.Contains(prompt, "- path: architecture/server.md"):
			calls = append(calls, "architecture-server")
			if err := writeTaskWikiOutput(command, outputRoot, "# Server Runtime\n"); err != nil {
				return CommandResult{}, err
			}
		default:
			t.Fatalf("unexpected page prompt: %s", prompt)
		}
		return CommandResult{}, nil
	})

	require.NoError(t, err)
	require.Equal(t, outputRoot, result.WikiDir)
	require.Equal(t, 4, result.TasksRun)
	require.Equal(t, []string{"planner", "home", "architecture-overview", "architecture-server"}, calls)
	require.FileExists(t, filepath.Join(outputRoot, "architecture", "overview.md"))
	require.FileExists(t, filepath.Join(outputRoot, "architecture", "server.md"))
}

func TestGenerateRunsWikiPageOpenCodeInIsolatedTaskDirLikeAnalysis(t *testing.T) {
	inputRoot := filepath.Join(t.TempDir(), "repo.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	writeFile(t, inputRoot, "README.md", "# Repo\n")

	result, err := Generate(context.Background(), Options{
		InputRoot:  inputRoot,
		OutputRoot: outputRoot,
	}, func(_ context.Context, command Command) (CommandResult, error) {
		if filepath.Base(command.Dir) == "planner" {
			require.Equal(t, command.Dir, argValue(command.Args, "--dir"))
			require.Empty(t, envValue(command.Env, "OPENCODE_CONFIG"))
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Home",
					Summary: "Overview.",
				},
			})
			return CommandResult{}, nil
		}

		require.True(t, strings.HasPrefix(command.Dir, filepath.Join(outputRoot, ".code-wiki")+string(os.PathSeparator)))
		require.Equal(t, command.Dir, argValue(command.Args, "--dir"))
		require.NotEqual(t, outputRoot, argValue(command.Args, "--dir"))
		require.Empty(t, envValue(command.Env, "OPENCODE_CONFIG"))
		require.Empty(t, envValue(command.Env, "OPENCODE_DISABLE_PROJECT_CONFIG"))
		requireOpenCodeTaskDenied(t, command.Dir)
		prompt := strings.Join(command.Args, "\n")
		require.Contains(t, prompt, "OpenCode runs in an isolated wiki page task directory")
		require.NotContains(t, prompt, "The current working directory is the wiki output root")
		require.Contains(t, prompt, filepath.Join(outputRoot, "README.md"))
		if err := writeWikiOutput(outputRoot, "README.md", "# Wiki\n"); err != nil {
			return CommandResult{}, err
		}
		return CommandResult{}, nil
	})

	require.NoError(t, err)
	require.Equal(t, outputRoot, result.WikiDir)
	require.Equal(t, 2, result.TasksRun)
	require.FileExists(t, filepath.Join(outputRoot, "README.md"))
}

func TestGenerateRunsWikiPagesWithConcurrentJobs(t *testing.T) {
	inputRoot := filepath.Join(t.TempDir(), "repo.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	writeFile(t, inputRoot, "README.md", "# Repo\n")
	writeFile(t, inputRoot, "server.go", "package main\n")
	writeFile(t, inputRoot, "client.go", "package main\n")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	started := make(chan struct{}, 3)
	release := make(chan struct{})
	var releaseOnce sync.Once
	releasePages := func() {
		releaseOnce.Do(func() {
			close(release)
		})
	}
	defer releasePages()

	done := make(chan struct {
		result Result
		err    error
	}, 1)

	var mu sync.Mutex
	activePages := 0
	maxActivePages := 0

	go func() {
		result, err := Generate(ctx, Options{
			InputRoot:              inputRoot,
			OutputRoot:             outputRoot,
			Jobs:                   2,
			IsolateOpenCodeRuntime: true,
		}, func(ctx context.Context, command Command) (CommandResult, error) {
			if filepath.Base(command.Dir) == "planner" {
				writeWikiPlan(t, command.Dir, WikiPlan{
					Schema: WikiPlanSchema,
					Homepage: WikiPage{
						ID:      "home",
						Path:    "README.md",
						Title:   "Home",
						Summary: "Overview.",
					},
					Sections: []WikiSection{
						{
							ID:      "runtime",
							Title:   "Runtime",
							Summary: "Runtime architecture.",
							Pages: []WikiPage{
								{
									ID:      "runtime-server",
									Path:    "runtime/server.md",
									Title:   "Server Runtime",
									Summary: "Server runtime path.",
								},
								{
									ID:      "runtime-client",
									Path:    "runtime/client.md",
									Title:   "Client Runtime",
									Summary: "Client runtime path.",
								},
							},
						},
					},
				})
				return CommandResult{}, nil
			}

			mu.Lock()
			activePages++
			if activePages > maxActivePages {
				maxActivePages = activePages
			}
			mu.Unlock()

			started <- struct{}{}
			select {
			case <-release:
			case <-ctx.Done():
				return CommandResult{}, ctx.Err()
			}

			prompt := strings.Join(command.Args, "\n")
			switch {
			case strings.Contains(prompt, "- path: README.md"):
				if err := writeTaskWikiOutput(command, outputRoot, "# Wiki\n"); err != nil {
					return CommandResult{}, err
				}
			case strings.Contains(prompt, "- path: runtime/server.md"):
				if err := writeTaskWikiOutput(command, outputRoot, "# Server Runtime\n"); err != nil {
					return CommandResult{}, err
				}
			case strings.Contains(prompt, "- path: runtime/client.md"):
				if err := writeTaskWikiOutput(command, outputRoot, "# Client Runtime\n"); err != nil {
					return CommandResult{}, err
				}
			default:
				t.Errorf("unexpected page prompt: %s", prompt)
			}

			mu.Lock()
			activePages--
			mu.Unlock()
			return CommandResult{}, nil
		})
		done <- struct {
			result Result
			err    error
		}{result: result, err: err}
	}()

	timedOut := false
	for i := 0; i < 2; i++ {
		select {
		case <-started:
		case <-time.After(500 * time.Millisecond):
			timedOut = true
		}
		if timedOut {
			break
		}
	}
	releasePages()
	outcome := <-done

	if timedOut {
		t.Fatalf("timed out waiting for two concurrent wiki page tasks")
	}
	require.NoError(t, outcome.err)
	require.Equal(t, 4, outcome.result.TasksRun)
	require.Equal(t, 2, maxActivePages)
	require.FileExists(t, filepath.Join(outputRoot, "README.md"))
	require.FileExists(t, filepath.Join(outputRoot, "runtime", "server.md"))
	require.FileExists(t, filepath.Join(outputRoot, "runtime", "client.md"))
}

func TestGenerateContinuesWikiPagesAfterPageFailure(t *testing.T) {
	inputRoot := filepath.Join(t.TempDir(), "repo.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	writeFile(t, inputRoot, "README.md", "# Repo\n")
	writeFile(t, inputRoot, "server.go", "package main\n")

	var calls []string
	result, err := Generate(context.Background(), Options{
		InputRoot:  inputRoot,
		OutputRoot: outputRoot,
		Jobs:       1,
	}, func(_ context.Context, command Command) (CommandResult, error) {
		if filepath.Base(command.Dir) == "planner" {
			calls = append(calls, "planner")
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Home",
					Summary: "Overview.",
				},
				Sections: []WikiSection{
					{
						ID:      "runtime",
						Title:   "Runtime",
						Summary: "Runtime architecture.",
						Pages: []WikiPage{
							{
								ID:      "runtime-server",
								Path:    "runtime/server.md",
								Title:   "Server Runtime",
								Summary: "Server runtime path.",
							},
						},
					},
				},
			})
			return CommandResult{}, nil
		}

		prompt := strings.Join(command.Args, "\n")
		switch {
		case strings.Contains(prompt, "- path: README.md"):
			calls = append(calls, "home")
			return CommandResult{}, nil
		case strings.Contains(prompt, "- path: runtime/server.md"):
			calls = append(calls, "runtime-server")
			if err := writeTaskWikiOutput(command, outputRoot, "# Server Runtime\n"); err != nil {
				return CommandResult{}, err
			}
			return CommandResult{}, nil
		default:
			t.Fatalf("unexpected page prompt: %s", prompt)
			return CommandResult{}, nil
		}
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "1 wiki page task(s) failed")
	require.Contains(t, err.Error(), "wiki page home did not create final output")
	require.Equal(t, outputRoot, result.WikiDir)
	require.Equal(t, 3, result.TasksRun)
	require.Equal(t, []string{"planner", "home", "runtime-server"}, calls)
	require.FileExists(t, filepath.Join(outputRoot, "runtime", "server.md"))
}

func TestGenerateAppliesTaskTimeoutToOpenCodeCommand(t *testing.T) {
	inputRoot := t.TempDir()
	outputRoot := t.TempDir()
	writeFile(t, inputRoot, "README.md", "# Analysis\n")

	_, err := Generate(context.Background(), Options{
		InputRoot:   inputRoot,
		OutputRoot:  outputRoot,
		TaskTimeout: time.Minute,
	}, func(ctx context.Context, command Command) (CommandResult, error) {
		deadline, ok := ctx.Deadline()
		require.True(t, ok)
		require.WithinDuration(t, time.Now().Add(time.Minute), deadline, 5*time.Second)
		if filepath.Base(command.Dir) == "planner" {
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Home",
					Summary: "Overview.",
				},
			})
			return CommandResult{}, nil
		}
		require.Contains(t, strings.Join(command.Args, "\n"), "- path: README.md")
		if err := writeTaskWikiOutput(command, outputRoot, "# Wiki\n"); err != nil {
			return CommandResult{}, err
		}
		return CommandResult{}, nil
	})

	require.NoError(t, err)
}

func TestGenerateFailsClearlyWhenPlannerDoesNotWritePlan(t *testing.T) {
	inputRoot := t.TempDir()
	outputRoot := t.TempDir()
	writeFile(t, inputRoot, "README.md", "# Analysis\n")

	var plannerDir string
	_, err := Generate(context.Background(), Options{
		InputRoot:  inputRoot,
		OutputRoot: outputRoot,
	}, func(_ context.Context, command Command) (CommandResult, error) {
		if filepath.Base(command.Dir) == "planner" {
			plannerDir = command.Dir
			return CommandResult{
				Stdout: []byte("planned in chat only\n"),
				Stderr: []byte("model stopped before writing plan\n"),
			}, nil
		}
		t.Fatal("page runner should not be called without a plan")
		return CommandResult{}, nil
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "wiki planner did not create wiki_plan.json")
	require.Equal(t, "planned in chat only\n", string(readFileBytes(t, filepath.Join(plannerDir, "opencode.stdout.log"))))
	require.Equal(t, "model stopped before writing plan\n", string(readFileBytes(t, filepath.Join(plannerDir, "opencode.stderr.log"))))
}

func TestWikiPlannerPromptRequiresWritingPlanFile(t *testing.T) {
	inputRoot := filepath.Join(t.TempDir(), "repo.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	prompt := buildWikiPlannerPrompt([]WikiInput{{Name: "repo", Root: inputRoot}}, outputRoot, "")

	require.Contains(t, prompt, "- repo: "+inputRoot)
	require.Contains(t, prompt, "- "+outputRoot)
	require.Contains(t, prompt, "Read the input directories directly")
	require.Contains(t, prompt, "analysis repos commonly store source-file analysis as Markdown files")
	require.NotContains(t, prompt, "wiki_plan_task.json")
	require.Contains(t, prompt, "Write exactly one file in the current working directory: ./wiki_plan.json")
	require.Contains(t, prompt, "Do not answer with the plan in chat instead of writing ./wiki_plan.json")
	require.Contains(t, prompt, `"schema": "`+WikiPlanSchema+`"`)
	require.Contains(t, prompt, `"homepage"`)
	require.Contains(t, prompt, `"sections"`)
	require.Contains(t, prompt, `"pages"`)
	require.Contains(t, prompt, "level-1 sections")
	require.Contains(t, prompt, "level-2 pages")
	require.Contains(t, prompt, "Do not include source_paths")
	require.Contains(t, prompt, "Do not compress complex domains")
	require.Contains(t, prompt, "After drafting wiki_plan.json, review it against the project structure and complexity")
	require.Contains(t, prompt, "Keep revising wiki_plan.json")
	require.NotContains(t, prompt, "2-6 focused level-2 pages")
	require.NotContains(t, prompt, `"source_paths"`)
}

func TestWikiPlannerPromptDoesNotPrescribeFixedSectionOrPageCounts(t *testing.T) {
	inputRoot := filepath.Join(t.TempDir(), "repo.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	prompt := buildWikiPlannerPrompt([]WikiInput{{Name: "repo", Root: inputRoot}}, outputRoot, "")

	require.NotRegexp(t, `\b\d+\s*[-–]\s*\d+\s+(?:focused\s+|narrower\s+)?(?:level-[12]\s+)?(?:sections|pages)\b`, prompt)
}

func TestGenerateFailsClearlyWhenPageTaskDoesNotWriteFinalOutput(t *testing.T) {
	inputRoot := t.TempDir()
	outputRoot := t.TempDir()
	writeFile(t, inputRoot, "README.md", "# Analysis\n")

	_, err := Generate(context.Background(), Options{
		InputRoot:  inputRoot,
		OutputRoot: outputRoot,
	}, func(_ context.Context, command Command) (CommandResult, error) {
		if filepath.Base(command.Dir) == "planner" {
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Home",
					Summary: "Overview.",
				},
			})
			return CommandResult{}, nil
		}
		return CommandResult{Stdout: []byte("page in chat only\n")}, nil
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "wiki page home did not create final output")
	require.Contains(t, err.Error(), filepath.Join(outputRoot, "README.md"))
}

func TestGenerateFailsClearlyWhenPageTaskOnlyLeavesStaleFinalOutput(t *testing.T) {
	inputRoot := t.TempDir()
	outputRoot := t.TempDir()
	writeFile(t, inputRoot, "README.md", "# Analysis\n")
	writeFile(t, outputRoot, "README.md", "# Old Wiki\n")
	oldInfo, err := os.Stat(filepath.Join(outputRoot, "README.md"))
	require.NoError(t, err)

	_, err = Generate(context.Background(), Options{
		InputRoot:  inputRoot,
		OutputRoot: outputRoot,
	}, func(_ context.Context, command Command) (CommandResult, error) {
		if filepath.Base(command.Dir) == "planner" {
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Home",
					Summary: "Overview.",
				},
			})
			return CommandResult{}, nil
		}
		return CommandResult{Stdout: []byte("empty worker\n")}, nil
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "wiki page home did not update final output")
	newInfo, statErr := os.Stat(filepath.Join(outputRoot, "README.md"))
	require.NoError(t, statErr)
	require.Equal(t, oldInfo.ModTime(), newInfo.ModTime())
}

func TestWikiPagePromptRequiresWritingAssignedOutputPath(t *testing.T) {
	inputRoot := filepath.Join(t.TempDir(), "repo.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	finalPath := filepath.Join(outputRoot, "README.md")
	prompt := buildWikiPagePrompt([]WikiInput{{Name: "repo", Root: inputRoot}}, outputRoot, WikiPage{
		ID:    "home",
		Path:  "README.md",
		Title: "Home",
	}, "")

	require.Contains(t, prompt, "- repo: "+inputRoot)
	require.Contains(t, prompt, "- "+outputRoot)
	require.Contains(t, prompt, "Read the input directories directly")
	require.Contains(t, prompt, "Use the page title, summary, and path to identify relevant input directories")
	require.Contains(t, prompt, "analysis repos commonly store source-file analysis as Markdown files")
	require.NotContains(t, prompt, "source_paths")
	require.NotContains(t, prompt, "source_path")
	require.NotContains(t, prompt, "Do not inspect or write the output root directly")
	require.Contains(t, prompt, "OpenCode runs in an isolated wiki page task directory")
	require.NotContains(t, prompt, "The current working directory is the wiki output root")
	require.Contains(t, prompt, "Mandatory completion workflow:")
	require.Contains(t, prompt, "Write the Markdown file to the exact final output path before sending any final response")
	require.Contains(t, prompt, "Run a filesystem check for that exact path")
	require.Contains(t, prompt, "Only after that check passes, send a short completion note")
	require.Contains(t, prompt, "A chat response without the file on disk is a failed task")
	require.Contains(t, prompt, "If the source evidence is incomplete, still write the best accurate page you can before exiting")
	require.NotContains(t, prompt, "relative to the current working directory")
	require.Contains(t, prompt, "Write the final wiki page directly to output_root/page.path")
	require.Contains(t, prompt, "Do not write page output under .code-wiki or any temporary task directory")
	require.Contains(t, prompt, finalPath)
	require.Contains(t, prompt, "Success condition: this task is not complete until this exact file exists on disk: "+finalPath)
	require.Contains(t, prompt, "Create parent directories for the final file when they do not already exist.")
	require.Contains(t, prompt, "Use the file-write/edit tool to create or replace that exact absolute path; do not rely on chat text as the output.")
	require.Contains(t, prompt, "After writing, run a direct filesystem check for the exact file path, such as `test -f`, `ls -la`, or an equivalent read/stat operation.")
	require.Contains(t, prompt, "If that check fails, write the file again before exiting.")
	require.NotContains(t, prompt, "page.md")
	require.Contains(t, prompt, "Do not answer with the page content in chat instead of writing the file")
}

func TestRunCommandDoesNotHangWhenSubprocessKeepsOutputPipeOpen(t *testing.T) {
	oldWaitDelay := commandWaitDelay
	commandWaitDelay = 20 * time.Millisecond
	t.Cleanup(func() {
		commandWaitDelay = oldWaitDelay
	})

	start := time.Now()
	result, err := RunCommand(context.Background(), Command{
		Name: "sh",
		Args: []string{"-c", "(sleep 2) & printf done"},
	})

	require.NoError(t, err)
	require.Equal(t, "done", string(result.Stdout))
	require.Less(t, time.Since(start), 500*time.Millisecond)
}

func TestGenerateRejectsConcurrentJobsWithoutRuntimeIsolation(t *testing.T) {
	inputRoot := t.TempDir()
	outputRoot := t.TempDir()
	writeFile(t, inputRoot, "README.md", "# Analysis\n")

	var called bool
	_, err := Generate(context.Background(), Options{
		InputRoot:  inputRoot,
		OutputRoot: outputRoot,
		Jobs:       2,
	}, func(context.Context, Command) (CommandResult, error) {
		called = true
		return CommandResult{}, fmt.Errorf("runner should not be called")
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "--jobs > 1 requires --isolate-opencode-runtime")
	require.False(t, called)
}

func TestGenerateIsolatesOpenCodeRuntimeForPlannerAndPages(t *testing.T) {
	inputRoot := filepath.Join(t.TempDir(), "repo.kb")
	outputRoot := filepath.Join(t.TempDir(), "wiki")
	writeFile(t, inputRoot, "README.md", "# Analysis\n")

	sourceDataHome := t.TempDir()
	sourceDataDir := filepath.Join(sourceDataHome, "opencode")
	require.NoError(t, os.MkdirAll(sourceDataDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(sourceDataDir, "auth.json"), []byte(`{"token":"secret"}`), 0o600))
	require.NoError(t, os.WriteFile(filepath.Join(sourceDataDir, "account.json"), []byte(`{"email":"dev@example.com"}`), 0o600))
	require.NoError(t, os.WriteFile(filepath.Join(sourceDataDir, "opencode.db"), []byte("global db"), 0o600))
	t.Setenv("XDG_DATA_HOME", sourceDataHome)

	var mu sync.Mutex
	var dataHomes []string
	var stateHomes []string
	result, err := Generate(context.Background(), Options{
		InputRoot:              inputRoot,
		OutputRoot:             outputRoot,
		Jobs:                   2,
		IsolateOpenCodeRuntime: true,
	}, func(_ context.Context, command Command) (CommandResult, error) {
		dataHome := envValue(command.Env, "XDG_DATA_HOME")
		stateHome := envValue(command.Env, "XDG_STATE_HOME")
		if dataHome == "" {
			return CommandResult{}, fmt.Errorf("missing XDG_DATA_HOME for %s", command.Dir)
		}
		if stateHome == "" {
			return CommandResult{}, fmt.Errorf("missing XDG_STATE_HOME for %s", command.Dir)
		}
		isolatedDataDir := filepath.Join(dataHome, "opencode")
		if got := string(readFileBytes(t, filepath.Join(isolatedDataDir, "auth.json"))); got != `{"token":"secret"}` {
			return CommandResult{}, fmt.Errorf("auth.json = %s", got)
		}
		if got := string(readFileBytes(t, filepath.Join(isolatedDataDir, "account.json"))); got != `{"email":"dev@example.com"}` {
			return CommandResult{}, fmt.Errorf("account.json = %s", got)
		}
		if fileExists(filepath.Join(isolatedDataDir, "opencode.db")) {
			return CommandResult{}, fmt.Errorf("copied global opencode database into isolated runtime")
		}

		mu.Lock()
		dataHomes = append(dataHomes, dataHome)
		stateHomes = append(stateHomes, stateHome)
		mu.Unlock()

		if filepath.Base(command.Dir) == "planner" {
			writeWikiPlan(t, command.Dir, WikiPlan{
				Schema: WikiPlanSchema,
				Homepage: WikiPage{
					ID:      "home",
					Path:    "README.md",
					Title:   "Home",
					Summary: "Overview.",
				},
				Sections: []WikiSection{
					{
						ID:      "runtime",
						Title:   "Runtime",
						Summary: "Runtime architecture.",
						Pages: []WikiPage{
							{
								ID:      "runtime-server",
								Path:    "runtime/server.md",
								Title:   "Server Runtime",
								Summary: "Server runtime path.",
							},
							{
								ID:      "runtime-client",
								Path:    "runtime/client.md",
								Title:   "Client Runtime",
								Summary: "Client runtime path.",
							},
						},
					},
				},
			})
			return CommandResult{}, nil
		}

		if err := writeTaskWikiOutput(command, outputRoot, "# Wiki\n"); err != nil {
			return CommandResult{}, err
		}
		return CommandResult{}, nil
	})

	require.NoError(t, err)
	require.Equal(t, 4, result.TasksRun)
	require.Len(t, dataHomes, 4)
	require.Len(t, stateHomes, 4)
	require.Len(t, uniqueStrings(dataHomes), 4)
	require.Len(t, uniqueStrings(stateHomes), 4)
}

func TestGenerateRejectsMissingInputAndOutput(t *testing.T) {
	_, err := Generate(context.Background(), Options{}, func(context.Context, Command) (CommandResult, error) {
		t.Fatal("runner should not be called")
		return CommandResult{}, nil
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "input is required")

	inputRoot := t.TempDir()
	writeFile(t, inputRoot, "main.go", "package main\n")
	_, err = Generate(context.Background(), Options{InputRoot: inputRoot}, func(context.Context, Command) (CommandResult, error) {
		t.Fatal("runner should not be called")
		return CommandResult{}, nil
	})

	require.Error(t, err)
	require.Contains(t, err.Error(), "output is required")
}

func writeWikiPlan(t *testing.T, workDir string, plan WikiPlan) {
	t.Helper()
	data, err := json.MarshalIndent(plan, "", "  ")
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(workDir, "wiki_plan.json"), append(data, '\n'), 0o644))
}

func requireOpenCodeTaskDenied(t *testing.T, workDir string) {
	t.Helper()
	data, err := os.ReadFile(filepath.Join(workDir, "opencode.json"))
	require.NoError(t, err)
	var config struct {
		Permission map[string]string `json:"permission"`
	}
	require.NoError(t, json.Unmarshal(data, &config))
	require.Equal(t, "deny", config.Permission["task"])
	require.Equal(t, "allow", config.Permission["external_directory"])
}

func writeFile(t *testing.T, root string, rel string, content string) {
	t.Helper()
	path := filepath.Join(root, filepath.FromSlash(rel))
	require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
	require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
}

func writeTaskWikiOutput(command Command, outputRoot string, content string) error {
	path, err := wikiPagePathFromPrompt(command)
	if err != nil {
		return err
	}
	return writeWikiOutput(outputRoot, path, content)
}

func wikiPagePathFromPrompt(command Command) (string, error) {
	prompt := strings.Join(command.Args, "\n")
	for _, line := range strings.Split(prompt, "\n") {
		if strings.HasPrefix(line, "- path: ") {
			path := strings.TrimSpace(strings.TrimPrefix(line, "- path: "))
			if path != "" {
				return path, nil
			}
		}
	}
	return "", os.ErrNotExist
}

func writeWikiOutput(outputRoot string, path string, content string) error {
	target := filepath.Join(outputRoot, filepath.FromSlash(path))
	if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
		return err
	}
	return os.WriteFile(target, []byte(content), 0o644)
}

func argValue(args []string, key string) string {
	for i, arg := range args {
		if arg == key && i+1 < len(args) {
			return args[i+1]
		}
	}
	return ""
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func readFileBytes(t *testing.T, path string) []byte {
	t.Helper()
	data, err := os.ReadFile(path)
	require.NoError(t, err)
	return data
}

func envValue(env []string, key string) string {
	prefix := key + "="
	for _, entry := range env {
		if strings.HasPrefix(entry, prefix) {
			return strings.TrimPrefix(entry, prefix)
		}
	}
	return ""
}

func uniqueStrings(values []string) []string {
	seen := map[string]struct{}{}
	for _, value := range values {
		seen[value] = struct{}{}
	}
	unique := make([]string, 0, len(seen))
	for value := range seen {
		unique = append(unique, value)
	}
	return unique
}
