package app

import (
	"bytes"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/pipeline"
)

func TestRunBuildTopPackageUsesCachedCompiledOutputs(t *testing.T) {
	root := t.TempDir()
	mkdirAll(t, filepath.Join(root, "build"))
	mkdirAll(t, filepath.Join(root, ".cache", "2da"))
	mkdirAll(t, filepath.Join(root, "topdata", "assets", "gui"))
	mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust"))

	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: test_module
topdata:
  source: topdata
  build: .cache
  package_hak: sow_top.hak
  package_tlk: sow_tlk.tlk
`)
	writeFile(t, filepath.Join(root, ".cache", "2da", "repadjust.2da"), "2DA V2.0\n\n      Label\n0     TEST_LABEL\n")
	writeFile(t, filepath.Join(root, "build", "sow_tlk.tlk"), "compiled tlk")
	writeFile(t, filepath.Join(root, "topdata", "assets", "gui", "testicon.png"), "icon-data")
	writeFile(t, filepath.Join(root, "topdata", "data", "repadjust", "base.json"), "{ this is intentionally invalid json }\n")

	sourceTime := time.Now().Add(-2 * time.Hour)
	outputTime := time.Now().Add(-1 * time.Hour)
	setTreeTime(t, filepath.Join(root, "topdata"), sourceTime)
	setFileTime(t, filepath.Join(root, ".cache", "2da", "repadjust.2da"), outputTime)
	setFileTime(t, filepath.Join(root, "build", "sow_tlk.tlk"), outputTime)

	var stdout bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &bytes.Buffer{},
		cwd:    root,
		args:   []string{"build-top-package"},
	}

	if err := runBuildTopPackage(ctx); err != nil {
		t.Fatalf("runBuildTopPackage failed: %v", err)
	}
	output := stdout.String()
	if !strings.Contains(output, "top package hak: build/sow_top.hak") {
		t.Fatalf("expected build-top-package output, got %q", output)
	}
	if strings.Contains(output, "[build-top-package]") {
		t.Fatalf("did not expect raw progress lines in normal output, got %q", output)
	}
	if _, err := os.Stat(filepath.Join(root, "build", "sow_top.hak")); err != nil {
		t.Fatalf("expected packaged hak output: %v", err)
	}
}

func TestRunBuildHAKsEmitsCompactSummary(t *testing.T) {
	root := t.TempDir()
	mkdirAll(t, filepath.Join(root, "assets", "envi", "music", "westgate"))
	mkdirAll(t, filepath.Join(root, "build"))
	mkdirAll(t, filepath.Join(root, "src"))

	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  source: src
  assets: assets
  build: build
haks:
  - name: envi
    priority: 1
    max_bytes: 1048576
    split: false
    include:
      - envi/**
`)
	writeFile(t, filepath.Join(root, "assets", "envi", "music", "westgate", "AleandAnecdotes.mp3"), "source-mp3")
	writeFile(t, filepath.Join(root, "assets", "envi", "music", "westgate", "CREDITS.md"), "# NWN Music Pack Credits\n\n| Artist | Title | Output File | Original File | Album | Date | License / Copyright | Notes |\n|---|---|---|---|---|---|---|---|\n| Darren Curtis | Ale and Anecdotes | `mus_wg_andnc.bmu` | `AleandAnecdotes.mp3` | Darren's Commercially Free Music |  | Darren Curtis Music |  |\n")

	ffprobePath := filepath.Join(root, "ffprobe")
	writeFile(t, ffprobePath, "#!/bin/sh\nprintf '%s\\n' '{\"format\":{\"tags\":{\"artist\":\"Unknown\",\"title\":\"Broken Metadata\"}}}'\n")
	if err := os.Chmod(ffprobePath, 0o755); err != nil {
		t.Fatalf("chmod ffprobe: %v", err)
	}

	ffmpegPath := filepath.Join(root, "ffmpeg")
	writeFile(t, ffmpegPath, "#!/bin/sh\nfor last; do :; done\nprintf 'bmu-data' > \"$last\"\n")
	if err := os.Chmod(ffmpegPath, 0o755); err != nil {
		t.Fatalf("chmod ffmpeg: %v", err)
	}

	t.Setenv("SOW_FFMPEG", ffmpegPath)
	t.Setenv("SOW_FFPROBE", ffprobePath)

	var stdout bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &bytes.Buffer{},
		cwd:    root,
		args:   []string{"build-haks"},
	}

	if err := runBuildHAKs(ctx); err != nil {
		t.Fatalf("runBuildHAKs failed: %v", err)
	}

	output := stdout.String()
	if !strings.Contains(output, "Build HAKs ----------") {
		t.Fatalf("expected build header, got %q", output)
	}
	if !strings.Contains(output, "mapped: 1 music file(s); use --verbose to list mappings") {
		t.Fatalf("expected compact mapping summary, got %q", output)
	}
	if strings.Contains(output, "AleandAnecdotes.mp3 -> mus_wg_andnc.bmu") {
		t.Fatalf("did not expect verbose mapping in normal mode, got %q", output)
	}
	if !strings.Contains(output, "manifest: build/haks.json") {
		t.Fatalf("expected relative manifest path, got %q", output)
	}
}

func setTreeTime(t *testing.T, root string, modTime time.Time) {
	t.Helper()
	err := filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		return os.Chtimes(path, modTime, modTime)
	})
	if err != nil {
		t.Fatalf("set tree time under %s: %v", root, err)
	}
}

func TestRunBuildHAKsVerboseListsMappings(t *testing.T) {
	root := t.TempDir()
	mkdirAll(t, filepath.Join(root, "assets", "envi", "music", "westgate"))
	mkdirAll(t, filepath.Join(root, "build"))
	mkdirAll(t, filepath.Join(root, "src"))

	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  source: src
  assets: assets
  build: build
haks:
  - name: envi
    priority: 1
    max_bytes: 1048576
    split: false
    include:
      - envi/**
`)
	writeFile(t, filepath.Join(root, "assets", "envi", "music", "westgate", "AleandAnecdotes.mp3"), "source-mp3")
	writeFile(t, filepath.Join(root, "assets", "envi", "music", "westgate", "CREDITS.md"), "# NWN Music Pack Credits\n\n| Artist | Title | Output File | Original File | Album | Date | License / Copyright | Notes |\n|---|---|---|---|---|---|---|---|\n| Darren Curtis | Ale and Anecdotes | `mus_wg_andnc.bmu` | `AleandAnecdotes.mp3` | Darren's Commercially Free Music |  | Darren Curtis Music |  |\n")

	ffprobePath := filepath.Join(root, "ffprobe")
	writeFile(t, ffprobePath, "#!/bin/sh\nprintf '%s\\n' '{\"format\":{\"tags\":{\"artist\":\"Unknown\",\"title\":\"Broken Metadata\"}}}'\n")
	if err := os.Chmod(ffprobePath, 0o755); err != nil {
		t.Fatalf("chmod ffprobe: %v", err)
	}

	ffmpegPath := filepath.Join(root, "ffmpeg")
	writeFile(t, ffmpegPath, "#!/bin/sh\nfor last; do :; done\nprintf 'bmu-data' > \"$last\"\n")
	if err := os.Chmod(ffmpegPath, 0o755); err != nil {
		t.Fatalf("chmod ffmpeg: %v", err)
	}

	t.Setenv("SOW_FFMPEG", ffmpegPath)
	t.Setenv("SOW_FFPROBE", ffprobePath)

	var stdout bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &bytes.Buffer{},
		cwd:    root,
		args:   []string{"build-haks", "--verbose"},
	}

	if err := runBuildHAKs(ctx); err != nil {
		t.Fatalf("runBuildHAKs failed: %v", err)
	}

	output := stdout.String()
	if !strings.Contains(output, "mappings:") {
		t.Fatalf("expected verbose mappings header, got %q", output)
	}
	if !strings.Contains(output, "AleandAnecdotes.mp3 -> mus_wg_andnc.bmu") {
		t.Fatalf("expected verbose mapping output, got %q", output)
	}
	if !strings.Contains(output, "wrote: envi (1 assets)") {
		t.Fatalf("expected verbose archive action, got %q", output)
	}
}

func TestTopdataConsoleSuppressesProgressInNormalMode(t *testing.T) {
	var stdout bytes.Buffer
	console := &topdataConsole{
		stdout:       &stdout,
		projectRoot:  "/workspace/project",
		projectName:  "Test Module",
		commandName:  "build-topdata",
		commandLabel: "Build Topdata",
		level:        logLevelNormal,
	}

	console.progress("Packaging compiled topdata resources into sow_top.hak...")

	if stdout.String() != "" {
		t.Fatalf("expected no normal-mode progress output, got %q", stdout.String())
	}
}

func TestSpinnerEnabledForHonorsPlainTTYMode(t *testing.T) {
	t.Setenv("SOW_TOOLS_TTY_MODE", "plain")
	if spinnerEnabledFor(&bytes.Buffer{}, logLevelNormal) {
		t.Fatal("expected plain tty mode to disable spinner output")
	}
}

func TestTopdataConsoleDebugProgressAndRelativePaths(t *testing.T) {
	var stdout bytes.Buffer
	console := &topdataConsole{
		stdout:       &stdout,
		projectRoot:  "/workspace/project",
		projectName:  "Test Module",
		commandName:  "deploy-wiki",
		commandLabel: "Deploy Wiki",
		level:        logLevelDebug,
	}

	console.progress("NodeBB wiki plan: create 1, update 2, skip 3, stale 4, archive 5, drift 0")
	console.emitWikiDeployResult(10, 1, 2, 3, 4, 5, 0, "/workspace/project/build/wiki/deploy-manifest.json")

	output := stdout.String()
	if !strings.Contains(output, "[debug] NodeBB wiki plan: create 1, update 2, skip 3, stale 4, archive 5, drift 0") {
		t.Fatalf("expected debug progress line, got %q", output)
	}
	if !strings.Contains(output, "archived: 5") {
		t.Fatalf("expected archived deploy count, got %q", output)
	}
	if !strings.Contains(output, "manifest: build/wiki/deploy-manifest.json") {
		t.Fatalf("expected relative deploy manifest path, got %q", output)
	}
}

func TestProjectConsoleSuppressesBuildModuleProgressInNormalMode(t *testing.T) {
	var stdout bytes.Buffer
	console := &projectConsole{
		stdout:       &stdout,
		projectRoot:  "/workspace/project",
		projectName:  "Test Module",
		commandName:  "build-module",
		commandLabel: "Build Module",
		level:        logLevelNormal,
	}

	console.progress("Writing module archive...")

	if stdout.String() != "" {
		t.Fatalf("expected no normal-mode progress output, got %q", stdout.String())
	}
}

func TestProjectConsoleEmitsRelativePaths(t *testing.T) {
	var stdout bytes.Buffer
	console := &projectConsole{
		stdout:       &stdout,
		projectRoot:  "/workspace/project",
		projectName:  "Test Module",
		commandName:  "compare",
		commandLabel: "Compare",
		level:        logLevelNormal,
	}

	console.emitCompareResult(pipeline.CompareResult{
		ModulePath: "/workspace/project/build/test.mod",
		HAKPaths:   []string{"/workspace/project/build/core.hak"},
		Checked:    42,
	})

	output := stdout.String()
	if !strings.Contains(output, "module: build/test.mod") {
		t.Fatalf("expected relative module path, got %q", output)
	}
}

func TestRunBuildModuleToolkitPreflightRefreshesManifestAndSkipsBrokenCompiler(t *testing.T) {
	root := t.TempDir()
	mkdirAll(t, filepath.Join(root, "src", "module"))
	mkdirAll(t, filepath.Join(root, "src", "placeables"))
	mkdirAll(t, filepath.Join(root, "src", "scripts"))
	mkdirAll(t, filepath.Join(root, "build"))
	mkdirAll(t, filepath.Join(root, "scripts"))

	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  source: src
  build: build
scripts:
  compiler:
    search:
      - tools/script-compiler/nwn_script_comp
`)
	writeFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{
  "file_type": "IFO ",
  "file_version": "V3.2",
  "root": {
    "struct_type": 0,
    "fields": [
      {"label": "Mod_Name", "type": "CExoString", "value": "Test Module"}
    ]
  }
}
`)
	writeFile(t, filepath.Join(root, "src", "placeables", "thing.utp.json"), `{
  "file_type": "UTP ",
  "file_version": "V3.2",
  "root": {
    "struct_type": 0,
    "fields": [
      {"label": "OnUsed", "type": "ResRef", "value": "use_thing"}
    ]
  }
}
`)
	writeFile(t, filepath.Join(root, "src", "scripts", "use_thing.nss"), "void main() {}\n")
	writeFile(t, filepath.Join(root, "scripts", "fetch-hak-manifest.sh"), "#!/usr/bin/env sh\nset -eu\nmkdir -p \"$(dirname \"$1\")\"\nprintf '{\"module_haks\":[\"sow_core\"],\"haks\":[]}\n' >\"$1\"\n")
	if err := os.Chmod(filepath.Join(root, "scripts", "fetch-hak-manifest.sh"), 0o755); err != nil {
		t.Fatalf("chmod fetch-hak-manifest.sh: %v", err)
	}

	badCompiler := filepath.Join(root, "bad-compiler.sh")
	writeFile(t, badCompiler, "#!/usr/bin/env sh\nexit 1\n")
	if err := os.Chmod(badCompiler, 0o755); err != nil {
		t.Fatalf("chmod bad compiler: %v", err)
	}
	t.Setenv("SOW_NWN_SCRIPT_COMPILER", badCompiler)
	t.Setenv("SOW_MODULE_SKIP_NSS_COMPILATION", "")

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &stderr,
		cwd:    root,
		args:   []string{"build-module"},
	}

	if err := runBuildModule(ctx); err != nil {
		t.Fatalf("runBuildModule failed: %v", err)
	}

	moduleSource := mustReadFile(t, filepath.Join(root, "src", "module", "module.ifo.json"))
	if !strings.Contains(moduleSource, "\"Mod_HakList\"") || !strings.Contains(moduleSource, "\"sow_core\"") {
		t.Fatalf("expected manifest-applied module source, got %q", moduleSource)
	}
	if _, err := os.Stat(filepath.Join(root, ".cache", "ncs", "use_thing.ncs")); !os.IsNotExist(err) {
		t.Fatalf("expected broken explicit compiler to skip compilation, got err=%v", err)
	}
	if !strings.Contains(stdout.String(), "script compilation: skipped") {
		t.Fatalf("expected skip-compilation summary in output, got %q", stdout.String())
	}
	if strings.Contains(stdout.String(), "[build-module]") {
		t.Fatalf("did not expect raw build-module progress in normal output, got %q", stdout.String())
	}
}

func TestRunBuildModuleToolkitPreflightInstallsCompilerWhenNeeded(t *testing.T) {
	root := t.TempDir()
	mkdirAll(t, filepath.Join(root, "src", "module"))
	mkdirAll(t, filepath.Join(root, "src", "placeables"))
	mkdirAll(t, filepath.Join(root, "src", "scripts"))
	mkdirAll(t, filepath.Join(root, "build"))
	mkdirAll(t, filepath.Join(root, "scripts"))

	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  source: src
  build: build
scripts:
  compiler:
    search:
      - tools/script-compiler/nwn_script_comp
`)
	writeFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{
  "file_type": "IFO ",
  "file_version": "V3.2",
  "root": {
    "struct_type": 0,
    "fields": [
      {"label": "Mod_Name", "type": "CExoString", "value": "Test Module"}
    ]
  }
}
`)
	writeFile(t, filepath.Join(root, "src", "placeables", "thing.utp.json"), `{
  "file_type": "UTP ",
  "file_version": "V3.2",
  "root": {
    "struct_type": 0,
    "fields": [
      {"label": "OnUsed", "type": "ResRef", "value": "use_thing"}
    ]
  }
}
`)
	writeFile(t, filepath.Join(root, "src", "scripts", "use_thing.nss"), "void main() {}\n")
	writeFile(t, filepath.Join(root, "scripts", "fetch-hak-manifest.sh"), "#!/usr/bin/env sh\nset -eu\nmkdir -p \"$(dirname \"$1\")\"\nprintf '{\"module_haks\":[\"sow_core\"],\"haks\":[]}\n' >\"$1\"\n")
	writeFile(t, filepath.Join(root, "scripts", "install-script-compiler.sh"), "#!/usr/bin/env sh\nset -eu\ninstall_dir=\"${SOW_SCRIPT_COMPILER_INSTALL_DIR:-./tools/script-compiler}\"\nmkdir -p \"$install_dir\"\ncat >\"$install_dir/nwn_script_comp\" <<'SH'\n#!/usr/bin/env sh\nset -eu\nif [ \"${1:-}\" = \"--help\" ]; then\n  exit 0\nfi\nout=\"\"\nsrc=\"\"\nwhile [ \"$#\" -gt 0 ]; do\n  case \"$1\" in\n    -o)\n      out=\"$2\"\n      shift 2\n      ;;\n    --dirs)\n      shift 2\n      ;;\n    *)\n      src=\"$1\"\n      shift\n      ;;\n  esac\ndone\nprintf 'compiled:%s\\n' \"$(basename \"$src\")\" >\"$out\"\nSH\nchmod +x \"$install_dir/nwn_script_comp\"\nprintf 'installed: %s\\n' \"$install_dir\"\n")
	if err := os.Chmod(filepath.Join(root, "scripts", "fetch-hak-manifest.sh"), 0o755); err != nil {
		t.Fatalf("chmod fetch-hak-manifest.sh: %v", err)
	}
	if err := os.Chmod(filepath.Join(root, "scripts", "install-script-compiler.sh"), 0o755); err != nil {
		t.Fatalf("chmod install-script-compiler.sh: %v", err)
	}
	t.Setenv("SOW_NWN_SCRIPT_COMPILER", "")
	t.Setenv("SOW_MODULE_SKIP_NSS_COMPILATION", "")

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &stderr,
		cwd:    root,
		args:   []string{"build-module"},
	}

	if err := runBuildModule(ctx); err != nil {
		t.Fatalf("runBuildModule failed: %v\nstdout:\n%s\nstderr:\n%s", err, stdout.String(), stderr.String())
	}

	compiledScript := filepath.Join(root, ".cache", "ncs", "use_thing.ncs")
	if _, err := os.Stat(compiledScript); err != nil {
		t.Fatalf("expected installed compiler to compile script: %v", err)
	}
	if !strings.Contains(stdout.String(), "script compilation: installed") {
		t.Fatalf("expected install summary in output, got %q", stdout.String())
	}
	if strings.Contains(stdout.String(), "[build-module]") {
		t.Fatalf("did not expect raw build-module progress in normal output, got %q", stdout.String())
	}
}

func TestRunMusicScanUsesConfiguredDatasetsWithoutWriting(t *testing.T) {
	root := t.TempDir()
	mkdirAll(t, filepath.Join(root, "assets", "audio", "westgate"))
	mkdirAll(t, filepath.Join(root, "build"))
	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  assets: assets
  build: build
music:
  datasets:
    westgate_audio:
      source: audio/westgate
      output: generated/music
      prefix: wg_
`)
	writeFile(t, filepath.Join(root, "assets", "audio", "westgate", "Theme Song.mp3"), "source-mp3")

	var stdout bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &bytes.Buffer{},
		cwd:    root,
		args:   []string{"music", "scan", "--dataset", "westgate_audio"},
	}

	if err := runMusic(ctx); err != nil {
		t.Fatalf("runMusic failed: %v", err)
	}
	output := stdout.String()
	if !strings.Contains(output, "music scan") || !strings.Contains(output, "tracks: 1") {
		t.Fatalf("unexpected music scan output: %q", output)
	}
	if _, err := os.Stat(filepath.Join(root, ".cache", "credits")); !os.IsNotExist(err) {
		t.Fatalf("music scan should not write credits artifacts, stat err=%v", err)
	}
}

func TestRunMusicListDatasets(t *testing.T) {
	root := t.TempDir()
	mkdirAll(t, filepath.Join(root, "assets"))
	mkdirAll(t, filepath.Join(root, "build"))
	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  assets: assets
music:
  datasets:
    westgate_audio:
      source: audio/westgate
      output: generated/music
      prefix: wg_
`)

	var stdout bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &bytes.Buffer{},
		cwd:    root,
		args:   []string{"music", "list-datasets"},
	}

	if err := runMusic(ctx); err != nil {
		t.Fatalf("runMusic failed: %v", err)
	}
	if !strings.Contains(stdout.String(), "westgate_audio: source=audio/westgate output=generated/music prefix=wg_") {
		t.Fatalf("unexpected dataset list: %q", stdout.String())
	}
}

func TestRunConfigEffectiveJSONShowsDefaultsAndProvenance(t *testing.T) {
	root := t.TempDir()
	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  source: src
`)

	var stdout bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &bytes.Buffer{},
		cwd:    root,
		args:   []string{"config", "effective", "--json"},
	}

	if err := runConfig(ctx); err != nil {
		t.Fatalf("runConfig failed: %v", err)
	}
	output := stdout.String()
	if !strings.Contains(output, `"hak_manifest": "haks.json"`) {
		t.Fatalf("expected HAK manifest default in effective config, got %q", output)
	}
	if !strings.Contains(output, `"paths.build":`) || !strings.Contains(output, `"toolkit default"`) {
		t.Fatalf("expected default provenance in effective config, got %q", output)
	}
}

func TestRunConfigExplainReportsYAMLSource(t *testing.T) {
	root := t.TempDir()
	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  build: output
`)

	var stdout bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &bytes.Buffer{},
		cwd:    root,
		args:   []string{"config", "explain", "paths.build"},
	}

	if err := runConfig(ctx); err != nil {
		t.Fatalf("runConfig failed: %v", err)
	}
	output := stdout.String()
	if !strings.Contains(output, "value: \"output\"") {
		t.Fatalf("expected configured build value, got %q", output)
	}
	if !strings.Contains(output, "source: yaml") {
		t.Fatalf("expected YAML source, got %q", output)
	}
}

func TestRunConfigValidateDoesNotScanSourceInventory(t *testing.T) {
	root := t.TempDir()
	mkdirAll(t, filepath.Join(root, "build"))
	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  source: missing-src
  build: build
`)

	var stdout bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &bytes.Buffer{},
		cwd:    root,
		args:   []string{"config", "validate"},
	}

	if err := runConfig(ctx); err != nil {
		t.Fatalf("runConfig failed: %v", err)
	}
	if !strings.Contains(stdout.String(), "config: ok") {
		t.Fatalf("expected config validation output, got %q", stdout.String())
	}
}

func TestRunConfigSourcesListsActiveOverrides(t *testing.T) {
	root := t.TempDir()
	writeFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
  name: Test Module
  resref: testmod
paths:
  source: src
`)
	t.Setenv("SOW_BUILD_HAKS_KEEP_EXISTING", "1")

	var stdout bytes.Buffer
	ctx := context{
		stdout: &stdout,
		stderr: &bytes.Buffer{},
		cwd:    root,
		args:   []string{"config", "sources"},
	}

	if err := runConfig(ctx); err != nil {
		t.Fatalf("runConfig failed: %v", err)
	}
	output := stdout.String()
	if !strings.Contains(output, "active overrides:") {
		t.Fatalf("expected active overrides section, got %q", output)
	}
	if !strings.Contains(output, "build.keep_existing_haks=1") {
		t.Fatalf("expected keep existing override, got %q", output)
	}
}

func TestInlineFlagParsersRejectEmptyValues(t *testing.T) {
	if _, err := parseBuildHAKArgs([]string{"--hak="}); err == nil || !strings.Contains(err.Error(), "--hak requires a value") {
		t.Fatalf("expected empty --hak inline value error, got %v", err)
	}
	if _, err := parseMusicCommandArgs([]string{"--dataset="}); err == nil || !strings.Contains(err.Error(), "--dataset requires a value") {
		t.Fatalf("expected empty --dataset inline value error, got %v", err)
	}
	if _, err := parseDeployWikiArgs("deploy-wiki", []string{"--endpoint="}); err == nil || !strings.Contains(err.Error(), "--endpoint requires a value") {
		t.Fatalf("expected empty --endpoint inline value error, got %v", err)
	}
	if _, err := parseBuildChangelogArgs("build-changelog", []string{"--output="}); err == nil || !strings.Contains(err.Error(), "--output requires a value") {
		t.Fatalf("expected empty --output inline value error, got %v", err)
	}
}

func TestParseBuildChangelogArgsAcceptsInlineValues(t *testing.T) {
	opts, err := parseBuildChangelogArgs("build-changelog", []string{
		"--config=scripts/changelog.json",
		"--output=CHANGELOG.md",
		"--current-tag=v2",
		"--previous-tag=v1",
		"--api-base-url=https://gitea.example/api/v1",
		"--token=secret",
	})
	if err != nil {
		t.Fatalf("parseBuildChangelogArgs failed: %v", err)
	}
	if opts.configPath != "scripts/changelog.json" ||
		opts.outputPath != "CHANGELOG.md" ||
		opts.currentTag != "v2" ||
		opts.previousTag != "v1" ||
		opts.apiBaseURL != "https://gitea.example/api/v1" ||
		opts.token != "secret" {
		t.Fatalf("unexpected changelog opts: %#v", opts)
	}
}

func mkdirAll(t *testing.T, path string) {
	t.Helper()
	if err := os.MkdirAll(path, 0o755); err != nil {
		t.Fatalf("mkdir %s: %v", path, err)
	}
}

func writeFile(t *testing.T, path, content string) {
	t.Helper()
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
}

func setFileTime(t *testing.T, path string, modTime time.Time) {
	t.Helper()
	if err := os.Chtimes(path, modTime, modTime); err != nil {
		t.Fatalf("set file time %s: %v", path, err)
	}
}

func mustReadFile(t *testing.T, path string) string {
	t.Helper()
	raw, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read %s: %v", path, err)
	}
	return string(raw)
}
