package indexer

import (
	"os"
	"path/filepath"
	"sort"
	"testing"

	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/zzet/gortex/internal/config"
	"github.com/zzet/gortex/internal/graph"
	"github.com/zzet/gortex/internal/modules"
	"github.com/zzet/gortex/internal/parser"
)

// writeWorkspaceFile materialises a manifest under dir at a
// repo-relative slash path, creating intermediate directories — the
// shared writeFile helper expects an already-existing parent dir.
func writeWorkspaceFile(t *testing.T, dir, rel, content string) {
	t.Helper()
	abs := filepath.Join(dir, filepath.FromSlash(rel))
	require.NoError(t, os.MkdirAll(filepath.Dir(abs), 0o755))
	writeFile(t, abs, content)
}

// workspaceMemberTargets indexes dir and returns the sorted To
// endpoints of every EdgePackageWorkspaceMember edge in the graph.
func workspaceMemberTargets(t *testing.T, dir string) []string {
	t.Helper()
	g := graph.New()
	idx := New(g, parser.NewRegistry(), config.Default().Index, zap.NewNop())
	_, err := idx.Index(dir)
	require.NoError(t, err)

	var targets []string
	for _, e := range g.AllEdges() {
		if e.Kind == graph.EdgePackageWorkspaceMember {
			targets = append(targets, e.To)
		}
	}
	sort.Strings(targets)
	return targets
}

// TestIndex_PackageWorkspaceMembership pins index-time materialisation
// of root→member edges across all three package-manager ecosystems,
// including glob and explicit members, plus the no-regression case.
func TestIndex_PackageWorkspaceMembership(t *testing.T) {
	cases := []struct {
		name  string
		files map[string]string
		want  []string // sorted EdgePackageWorkspaceMember targets
	}{
		{
			name: "npm workspace, glob plus explicit member",
			files: map[string]string{
				"package.json":              `{"name":"root","workspaces":["packages/*","apps/web"]}`,
				"packages/ui/package.json":   `{"name":"ui"}`,
				"packages/core/package.json": `{"name":"core"}`,
				"apps/web/package.json":      `{"name":"web"}`,
			},
			want: []string{
				"apps/web/package.json",
				"packages/core/package.json",
				"packages/ui/package.json",
			},
		},
		{
			name: "pnpm workspace, glob members",
			files: map[string]string{
				"pnpm-workspace.yaml":     "packages:\n  - 'packages/*'\n",
				"packages/a/package.json": `{"name":"a"}`,
				"packages/b/package.json": `{"name":"b"}`,
			},
			want: []string{
				"packages/a/package.json",
				"packages/b/package.json",
			},
		},
		{
			name: "cargo workspace, glob plus explicit member",
			files: map[string]string{
				"Cargo.toml":               "[workspace]\nmembers = [\"crates/*\", \"tools/gen\"]\n",
				"crates/engine/Cargo.toml": "[package]\nname = \"engine\"\n",
				"crates/cli/Cargo.toml":    "[package]\nname = \"cli\"\n",
				"tools/gen/Cargo.toml":     "[package]\nname = \"gen\"\n",
			},
			want: []string{
				"crates/cli/Cargo.toml",
				"crates/engine/Cargo.toml",
				"tools/gen/Cargo.toml",
			},
		},
		{
			name: "non-workspace repo produces no membership edges",
			files: map[string]string{
				"package.json": `{"name":"solo","version":"1.0.0","dependencies":{"react":"^18"}}`,
				"index.js":     "module.exports = 1;\n",
			},
			want: nil,
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			dir := t.TempDir()
			for rel, content := range c.files {
				writeWorkspaceFile(t, dir, rel, content)
			}
			got := workspaceMemberTargets(t, dir)
			if len(got) != len(c.want) {
				t.Fatalf("got %d membership edges %v, want %d %v", len(got), got, len(c.want), c.want)
			}
			for i := range got {
				if got[i] != c.want[i] {
					t.Errorf("membership edge[%d] = %q, want %q", i, got[i], c.want[i])
				}
			}
		})
	}
}

// TestIndex_PackageWorkspaceRootNode verifies the synthetic
// workspace-root node carries the expected kind and meta and that
// every membership edge originates from it.
func TestIndex_PackageWorkspaceRootNode(t *testing.T) {
	dir := t.TempDir()
	writeWorkspaceFile(t, dir, "package.json", `{"name":"root","workspaces":["packages/*"]}`)
	writeWorkspaceFile(t, dir, "packages/a/package.json", `{"name":"a"}`)
	writeWorkspaceFile(t, dir, "packages/b/package.json", `{"name":"b"}`)

	g := graph.New()
	idx := New(g, parser.NewRegistry(), config.Default().Index, zap.NewNop())
	_, err := idx.Index(dir)
	require.NoError(t, err)

	rootID := modules.WorkspaceRootNodeID(modules.WorkspaceNPM, ".")
	root := g.GetNode(rootID)
	require.NotNil(t, root, "workspace root node %s must be materialised", rootID)
	require.Equal(t, graph.KindPackage, root.Kind)
	if v, _ := root.Meta["package_workspace"].(bool); !v {
		t.Errorf("root node should carry package_workspace=true meta")
	}
	require.Equal(t, "npm", root.Meta["ecosystem"])

	var memberEdges int
	for _, e := range g.AllEdges() {
		if e.Kind != graph.EdgePackageWorkspaceMember {
			continue
		}
		memberEdges++
		require.Equal(t, rootID, e.From, "membership edge must originate at the workspace root")
		require.Equal(t, graph.OriginASTResolved, e.Origin)
	}
	require.Equal(t, 2, memberEdges, "expected one membership edge per resolved member")
}
