package taskboard

import (
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"reflect"
	"regexp"
	"runtime"
	"sort"
	"strings"
	"testing"

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

func taskboardRepoRoot(t *testing.T) string {
	t.Helper()
	_, filename, _, ok := runtime.Caller(0)
	require.True(t, ok)
	return filepath.Clean(filepath.Join(filepath.Dir(filename), "..", "..", ".."))
}

// TestTaskBoardPublicContractVocabulary verifies the exact field/key sets of
// Task-owned public DTOs, client types, config, and navigation items.
func TestTaskBoardPublicContractVocabulary(t *testing.T) {
	repoRoot := taskboardRepoRoot(t)

	// Task-owned public target is a closed vocabulary. Each
	// shape below is parsed at test time and its field/key set is compared
	// to an allowlist of the current Task target. The allowlist captures
	// status-driven lifecycle (todo/in_progress/in_review/done), resolved
	// Task identity (assignee/creator), claim/unclaim, block/unblock,
	// checkpoint, artifact, timeline, handoff, subtask, kind, priority,
	// owner-quote source refs, and the supporting read-state / progress /
	// collaboration metadata that the Task target already exposes.
	//
	// Adding a new field to any of these shapes is a deliberate target
	// extension: update the allowlist in this test as part of the same
	// change. The test fails closed otherwise.

	t.Run("dto Task shape", func(t *testing.T) {
		actual := parseGoStructJSONKeys(t,
			filepath.Join(repoRoot, "internal", "api", "dto", "task_browser.go"),
			"Task")
		expected := setOf(
			"id", "taskScopeId", "channelId", "taskNumber", "title", "status",
			"description", "kind", "priority", "blockedReason",
			"lastActivityAt", "artifactCount", "parentTaskRef", "discussionCount",
			"assigneeProfile", "kindField", "priorityField",
			"checkpointProgress", "subtaskProgress",
			"createdAt", "createdByProfile", "createdByType", "createdById", "createdByName",
			"source", "sourceThread", "readState",
			"assignedAt", "completedAt",
		)
		assertSetEqual(t, "dto.Task JSON keys", expected, actual)
	})

	t.Run("dto TaskDetail shape", func(t *testing.T) {
		actual := parseGoStructJSONKeys(t,
			filepath.Join(repoRoot, "internal", "api", "dto", "task_browser.go"),
			"TaskDetail")
		expected := setOf(
			"task", "parentTask", "checkpoints",
			"timeline", "timelineHasMore", "timelineNextCursor",
			"acceptanceCriteria", "reviewChecklist",
			"subtasks", "subtasksHasMore", "subtasksNextCursor",
			"pinnedMessageRefs", "pinnedMessageRefsHasMore",
			"relevantURLs", "relevantURLsHasMore",
			"workspaceRefs", "workspaceRefsHasMore",
			"artifacts",
		)
		assertSetEqual(t, "dto.TaskDetail JSON keys", expected, actual)
	})

	t.Run("e2e client Task shape", func(t *testing.T) {
		actual := parseGoStructJSONKeys(t,
			filepath.Join(repoRoot, "e2e", "client", "task_client.go"),
			"Task")
		expected := setOf("id", "channelId", "taskNumber", "title", "status")
		assertSetEqual(t, "e2e client.Task JSON keys", expected, actual)
	})

	t.Run("web Task type shape", func(t *testing.T) {
		actual := parseTSTypeProperties(t,
			filepath.Join(repoRoot, "web", "src", "types", "tasks.ts"),
			"Task")
		expected := setOf(
			"id", "taskScopeId", "channelId", "taskNumber", "title", "status",
			"description", "kind", "priority", "blockedReason",
			"lastActivityAt", "artifactCount", "parentTaskRef", "discussionCount",
			"assigneeProfile", "kindField", "priorityField",
			"checkpointProgress", "subtaskProgress",
			"createdAt", "createdByProfile", "createdByType", "createdById", "createdByName",
			"source", "sourceThread", "readState",
			"assignedAt", "completedAt",
		)
		assertSetEqual(t, "web Task type properties", expected, actual)
	})

	t.Run("web PublicConfig type shape", func(t *testing.T) {
		actual := parseTSTypeProperties(t,
			filepath.Join(repoRoot, "web", "src", "agentteam", "api", "core.ts"),
			"PublicConfig")
		expected := setOf(
			"pingcapAuthEnabled", "passwordAuthEnabled",
			"openRegistration", "registrationMode",
			"messageHistoryDays", "messageSearchEnabled",
			"teamsEnabled",
			"organizationDirectoryEnabled", "organizationByokEnabled",
			"billingEnabled",
			"desktopQuickStartEnabled",
			"daemonConnectBaseURL",
			"meshXEnabled", "meshXKnowledgeBasesEnabled", "meshXWorkflowsEnabled", "meshXMiniappPairingEnabled", "meshXRolesEnabled", "meshXBaseURL",
			"serverVersion", "serverGitHash", "deploymentEnvironment", "serverBuildTime",
			"coreSchemaVersion",
		)
		assertSetEqual(t, "web PublicConfig type properties", expected, actual)
	})

	t.Run("web taskboard system view IDs", func(t *testing.T) {
		actual := parseTaskboardSystemViewIDs(t,
			filepath.Join(repoRoot, "web", "src", "agentteam", "workspace", "tasks", "taskboard", "taskboardViews.ts"))
		expected := setOf(
			"all-tasks", "related-to-me", "created-by-me", "blocked",
		)
		assertSetEqual(t, "taskboard system view IDs", expected, actual)
	})
}

func setOf(values ...string) map[string]struct{} {
	out := make(map[string]struct{}, len(values))
	for _, v := range values {
		out[v] = struct{}{}
	}
	return out
}

func assertSetEqual(t *testing.T, label string, expected, actual map[string]struct{}) {
	t.Helper()
	var missing []string
	for k := range expected {
		if _, ok := actual[k]; !ok {
			missing = append(missing, k)
		}
	}
	var extra []string
	for k := range actual {
		if _, ok := expected[k]; !ok {
			extra = append(extra, k)
		}
	}
	if len(missing) == 0 && len(extra) == 0 {
		return
	}
	sort.Strings(missing)
	sort.Strings(extra)
	t.Fatalf("%s set mismatch\n  missing from source (remove from allowlist or restore field): %s\n"+
		"  extra in source (add to allowlist or drop field): %s",
		label, strings.Join(missing, ", "), strings.Join(extra, ", "))
}

func parseGoStructJSONKeys(t *testing.T, path, structName string) map[string]struct{} {
	t.Helper()
	fset := token.NewFileSet()
	file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
	require.NoError(t, err)
	keys := make(map[string]struct{})
	for _, decl := range file.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok {
			continue
		}
		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok || typeSpec.Name == nil || typeSpec.Name.Name != structName {
				continue
			}
			structType, ok := typeSpec.Type.(*ast.StructType)
			if !ok {
				continue
			}
			for _, field := range structType.Fields.List {
				if field.Tag == nil {
					continue
				}
				tag := reflect.StructTag(strings.Trim(field.Tag.Value, "`"))
				jsonTag, ok := tag.Lookup("json")
				if !ok {
					continue
				}
				name := strings.Split(jsonTag, ",")[0]
				if name == "" || name == "-" {
					continue
				}
				keys[name] = struct{}{}
			}
		}
	}
	require.NotEmpty(t, keys, "no JSON-tagged fields parsed for %s in %s", structName, path)
	return keys
}

var tsTypeHeaderRe = regexp.MustCompile(`^(?:export\s+)?type\s+(\w+)\s*=\s*\{\s*$`)

func parseTSTypeProperties(t *testing.T, path, typeName string) map[string]struct{} {
	t.Helper()
	data, err := os.ReadFile(path)
	require.NoError(t, err)
	lines := strings.Split(string(data), "\n")

	startIdx := -1
	for i, line := range lines {
		m := tsTypeHeaderRe.FindStringSubmatch(strings.TrimSpace(line))
		if len(m) == 2 && m[1] == typeName {
			startIdx = i + 1
			break
		}
	}
	require.GreaterOrEqualf(t, startIdx, 0, "TS type %s header not found in %s", typeName, path)

	props := make(map[string]struct{})
	propertyRe := regexp.MustCompile(`^\s+([A-Za-z_$][\w$]*)\s*\??\s*:`)
	depth := 0
	for i := startIdx; i < len(lines); i++ {
		trimmed := strings.TrimSpace(lines[i])
		if trimmed == "}" && depth == 0 {
			break
		}
		// Track nested literal braces so a multi-line inline object value on
		// a property line does not prematurely close the type block.
		for _, r := range lines[i] {
			switch r {
			case '{':
				depth++
			case '}':
				if depth > 0 {
					depth--
				}
			}
		}
		m := propertyRe.FindStringSubmatch(lines[i])
		if len(m) == 2 {
			props[m[1]] = struct{}{}
		}
	}
	require.NotEmpty(t, props, "no properties parsed for TS type %s in %s", typeName, path)
	return props
}

var tsConstHeaderRe = regexp.MustCompile(`^export\s+const\s+(\w+)\s*[:=]`)
var idValueRe = regexp.MustCompile(`^\s*id\s*:\s*"([^"]+)"`)

func parseTaskboardSystemViewIDs(t *testing.T, path string) map[string]struct{} {
	t.Helper()
	data, err := os.ReadFile(path)
	require.NoError(t, err)
	lines := strings.Split(string(data), "\n")

	startIdx := -1
	for i, line := range lines {
		m := tsConstHeaderRe.FindStringSubmatch(strings.TrimSpace(line))
		if len(m) == 2 && m[1] == "taskboardSystemViews" {
			startIdx = i + 1
			break
		}
	}
	require.GreaterOrEqualf(t, startIdx, 0, "taskboardSystemViews not found in %s", path)

	ids := make(map[string]struct{})
	for i := startIdx; i < len(lines); i++ {
		trimmed := strings.TrimSpace(lines[i])
		if trimmed == "]" {
			break
		}
		if m := idValueRe.FindStringSubmatch(lines[i]); len(m) == 2 {
			ids[m[1]] = struct{}{}
		}
	}
	require.NotEmpty(t, ids, "no system view IDs parsed from %s", path)
	return ids
}
