package detect

import (
	"os"
	"path/filepath"
	"runtime"
	"testing"
)

type jsonFixture struct {
	Name string `json:"name"`
}

type yamlFixture struct {
	Enabled bool `yaml:"enabled"`
}

type tomlFixture struct {
	Name string `toml:"name"`
}

func TestParseJSONFileStrictUnknownField(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	path := filepath.Join(root, "cfg.json")
	if err := os.WriteFile(path, []byte(`{"name":"ok","extra":true}`), 0o600); err != nil {
		t.Fatalf("write fixture: %v", err)
	}

	var parsed jsonFixture
	parseErr := ParseJSONFileStrict("detector", root, "cfg.json", &parsed)
	if parseErr == nil {
		t.Fatal("expected parse error")
		return
	}
	got := *parseErr
	if got.Detector != "detector" || got.Format != "json" {
		t.Fatalf("unexpected parse error shape: %#v", got)
	}
}

func TestParseJSONFileRejectsTrailingTopLevelDocument(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	path := filepath.Join(root, "cfg.json")
	if err := os.WriteFile(path, []byte(`{"name":"ok"}{"name":"extra"}`), 0o600); err != nil {
		t.Fatalf("write fixture: %v", err)
	}

	var parsed jsonFixture
	parseErr := ParseJSONFile("detector", root, "cfg.json", &parsed)
	if parseErr == nil {
		t.Fatal("expected parse error for trailing JSON document")
		return
	}
	got := *parseErr
	if got.Format != "json" {
		t.Fatalf("unexpected parse error format: %#v", got)
	}
}

func TestParseJSONFileAllowUnknownFields(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	path := filepath.Join(root, "cfg.json")
	if err := os.WriteFile(path, []byte(`{"name":"ok","extra":true}`), 0o600); err != nil {
		t.Fatalf("write fixture: %v", err)
	}

	var parsed jsonFixture
	parseErr := ParseJSONFileTolerant("detector", root, "cfg.json", &parsed)
	if parseErr != nil {
		t.Fatalf("expected no parse error, got %#v", parseErr)
	}
	if parsed.Name != "ok" {
		t.Fatalf("unexpected parsed result: %#v", parsed)
	}
}

func TestParseYAMLFileStrictUnknownField(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	path := filepath.Join(root, "cfg.yaml")
	if err := os.WriteFile(path, []byte("enabled: true\nextra: 1\n"), 0o600); err != nil {
		t.Fatalf("write fixture: %v", err)
	}

	var parsed yamlFixture
	parseErr := ParseYAMLFileStrict("detector", root, "cfg.yaml", &parsed)
	if parseErr == nil {
		t.Fatal("expected parse error")
		return
	}
	got := *parseErr
	if got.Format != "yaml" {
		t.Fatalf("unexpected parse error format: %#v", got)
	}
}

func TestParseYAMLFileAllowUnknownFields(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	path := filepath.Join(root, "cfg.yaml")
	if err := os.WriteFile(path, []byte("enabled: true\nextra: 1\n"), 0o600); err != nil {
		t.Fatalf("write fixture: %v", err)
	}

	var parsed yamlFixture
	parseErr := ParseYAMLFileTolerant("detector", root, "cfg.yaml", &parsed)
	if parseErr != nil {
		t.Fatalf("expected no parse error, got %#v", parseErr)
	}
	if !parsed.Enabled {
		t.Fatalf("unexpected parsed result: %#v", parsed)
	}
}

func TestParseTOMLFileStrictUnknownField(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	path := filepath.Join(root, "cfg.toml")
	if err := os.WriteFile(path, []byte("name = \"ok\"\nextra = true\n"), 0o600); err != nil {
		t.Fatalf("write fixture: %v", err)
	}

	var parsed tomlFixture
	parseErr := ParseTOMLFileStrict("detector", root, "cfg.toml", &parsed)
	if parseErr == nil {
		t.Fatal("expected parse error")
		return
	}
	got := *parseErr
	if got.Format != "toml" {
		t.Fatalf("unexpected parse error format: %#v", got)
	}
}

func TestParseTOMLFileAllowUnknownFields(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	path := filepath.Join(root, "cfg.toml")
	if err := os.WriteFile(path, []byte("name = \"ok\"\nextra = true\n"), 0o600); err != nil {
		t.Fatalf("write fixture: %v", err)
	}

	var parsed tomlFixture
	parseErr := ParseTOMLFileTolerant("detector", root, "cfg.toml", &parsed)
	if parseErr != nil {
		t.Fatalf("expected no parse error, got %#v", parseErr)
	}
	if parsed.Name != "ok" {
		t.Fatalf("unexpected parsed result: %#v", parsed)
	}
}

func TestStrictWrkrContractRejectsUnknownFields(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	path := filepath.Join(root, "wrkr.json")
	if err := os.WriteFile(path, []byte(`{"name":"ok","extra":"unexpected"}`), 0o600); err != nil {
		t.Fatalf("write fixture: %v", err)
	}

	var parsed jsonFixture
	parseErr := ParseJSONFileStrict("detector", root, "wrkr.json", &parsed)
	if parseErr == nil {
		t.Fatal("expected strict parse error")
		return
	}
	got := *parseErr
	if got.Format != "json" || got.Detector != "detector" {
		t.Fatalf("unexpected strict parse error: %#v", got)
	}
}

func TestReadFileWithinRootRejectsSymlinkEscape(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	outside := t.TempDir()
	target := filepath.Join(outside, "cfg.json")
	if err := os.WriteFile(target, []byte(`{"name":"outside"}`), 0o600); err != nil {
		t.Fatalf("write outside fixture: %v", err)
	}
	mustSymlinkOrSkip(t, target, filepath.Join(root, "cfg.json"))

	_, parseErr := ReadFileWithinRoot("detector", root, "cfg.json")
	if parseErr == nil {
		t.Fatal("expected unsafe_path parse error")
		return
	}
	got := *parseErr
	if got.Kind != "unsafe_path" {
		t.Fatalf("expected unsafe_path kind, got %#v", got)
	}
}

func TestReadFileWithinRootHandlesDanglingSymlinkDeterministically(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	mustSymlinkOrSkip(t, filepath.Join(root, "missing.json"), filepath.Join(root, "cfg.json"))

	_, parseErr := ReadFileWithinRoot("detector", root, "cfg.json")
	if parseErr == nil {
		t.Fatal("expected file_not_found parse error")
		return
	}
	got := *parseErr
	if got.Kind != "file_not_found" {
		t.Fatalf("expected file_not_found kind, got %#v", got)
	}
}

func TestReadFileWithinRootPermissionDenied(t *testing.T) {
	t.Parallel()

	if runtime.GOOS == "windows" {
		t.Skip("chmod-based permission fixture is not portable on windows")
	}

	root := t.TempDir()
	path := filepath.Join(root, "cfg.json")
	if err := os.WriteFile(path, []byte(`{"name":"blocked"}`), 0o600); err != nil {
		t.Fatalf("write fixture: %v", err)
	}
	if err := os.Chmod(path, 0o000); err != nil {
		t.Skipf("chmod unsupported in current environment: %v", err)
	}
	defer func() {
		_ = os.Chmod(path, 0o600)
	}()

	_, parseErr := ReadFileWithinRoot("detector", root, "cfg.json")
	if parseErr == nil {
		t.Fatal("expected permission parse error")
		return
	}
	got := *parseErr
	if got.Kind != "permission_denied" {
		t.Fatalf("expected permission_denied kind, got %#v", got)
	}
}

func TestWalkFilesWithParseErrorsRejectsExternalSymlinkedFile(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	outside := t.TempDir()
	target := filepath.Join(outside, "SKILL.md")
	if err := os.WriteFile(target, []byte("outside"), 0o600); err != nil {
		t.Fatalf("write outside fixture: %v", err)
	}
	mustSymlinkOrSkip(t, target, filepath.Join(root, ".agents", "skills", "deploy", "SKILL.md"))

	files, err := WalkFilesWithParseErrors("detector", root, Options{})
	if err != nil {
		t.Fatalf("walk files: %v", err)
	}
	if len(files) != 1 {
		t.Fatalf("expected one walked file, got %#v", files)
	}
	if files[0].Rel != ".agents/skills/deploy/SKILL.md" {
		t.Fatalf("unexpected walked file rel: %#v", files[0])
	}
	if files[0].ParseError == nil || files[0].ParseError.Kind != "unsafe_path" {
		t.Fatalf("expected unsafe_path parse error, got %#v", files[0].ParseError)
	}
}

func TestWalkFilesWithParseErrorsPreservesDeterministicOrderAndInRootSymlinks(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	mustWriteWalkFixture(t, root, "b.txt", "b")
	mustWriteWalkFixture(t, root, "nested/a.txt", "a")
	mustWriteWalkFixture(t, root, "node_modules/pkg/generated.txt", "generated")
	mustSymlinkOrSkip(t, filepath.Join(root, "nested", "a.txt"), filepath.Join(root, "alias.txt"))

	files, err := WalkFilesWithParseErrors("detector", root, Options{})
	if err != nil {
		t.Fatalf("walk files: %v", err)
	}

	if len(files) != 3 {
		t.Fatalf("expected three walked files after generated-path filtering, got %#v", files)
	}
	want := []string{"alias.txt", "b.txt", "nested/a.txt"}
	for i, rel := range want {
		if files[i].Rel != rel {
			t.Fatalf("expected rel %q at index %d, got %#v", rel, i, files)
		}
		if files[i].ParseError != nil {
			t.Fatalf("expected in-root file %q to remain readable, got %#v", rel, files[i].ParseError)
		}
	}
}

func TestWalkFilesWithParseErrorsHandlesDanglingSymlinkDeterministically(t *testing.T) {
	t.Parallel()

	root := t.TempDir()
	mustSymlinkOrSkip(t, filepath.Join(root, "missing.txt"), filepath.Join(root, "cfg.txt"))

	files, err := WalkFilesWithParseErrors("detector", root, Options{})
	if err != nil {
		t.Fatalf("walk files: %v", err)
	}
	if len(files) != 1 {
		t.Fatalf("expected one walked file, got %#v", files)
	}
	if files[0].ParseError == nil || files[0].ParseError.Kind != "file_not_found" {
		t.Fatalf("expected file_not_found parse error, got %#v", files[0].ParseError)
	}
}

func mustSymlinkOrSkip(t *testing.T, target, path string) {
	t.Helper()

	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("mkdir symlink parent: %v", err)
	}
	if err := os.Symlink(target, path); err != nil {
		t.Skipf("symlinks unsupported in this environment: %v", err)
	}
}

func mustWriteWalkFixture(t *testing.T, root, rel, content string) {
	t.Helper()
	path := filepath.Join(root, filepath.FromSlash(rel))
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("mkdir %s: %v", rel, err)
	}
	if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
		t.Fatalf("write %s: %v", rel, err)
	}
}
