package ipc

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/fs"
	"strings"
	"testing"

	"github.com/kami/maven/internal/store"
)

// mapErr turns a store sentinel into its wire twin so a module can errors.Is
// without importing internal/store. The design is right and the failure mode is
// quiet: add a sentinel to store, forget the switch, and the client gets an
// untyped error that no caller can branch on. These two tests are the alarm.

// mapErrPairs — every store sentinel that has a wire twin, and the twin.
var mapErrPairs = []struct {
	name  string // the store identifier, for the coverage test below
	from  error
	wants error
}{
	{"ErrNoFact", store.ErrNoFact, ErrNoFact},
	{"ErrConfidence", store.ErrConfidence, ErrConfidence},
	{"ErrVoidsMissing", store.ErrVoidsMissing, ErrVoidsMissing},
	{"ErrNudgeNotFound", store.ErrNudgeNotFound, ErrNudgeNotFound},
	{"ErrNudgeOutcome", store.ErrNudgeOutcome, ErrNudgeOutcome},
	{"ErrReminderNotFound", store.ErrReminderNotFound, ErrReminderNotFound},
	{"ErrReminderState", store.ErrReminderState, ErrReminderState},
	{"ErrReminderInFlight", store.ErrReminderInFlight, ErrReminderInFlight},
	{"ErrToolNotFound", store.ErrToolNotFound, ErrToolNotFound},
	{"ErrNoSuchTrace", store.ErrNoSuchTrace, ErrNoSuchTrace},
	{"ErrTaskNoDoneWhen", store.ErrTaskNoDoneWhen, ErrTaskNoDoneWhen},
	{"ErrTaskDuplicate", store.ErrTaskDuplicate, ErrTaskDuplicate},
	{"ErrTaskResolved", store.ErrTaskResolved, ErrTaskResolved},
}

// unmappedStoreErrors — store sentinels that deliberately have no wire twin,
// each with the reason it stays store-side. A new sentinel is in neither list
// and fails TestMapErrCoversEveryStoreSentinel, which is the point: whether a
// module can branch on an error is a decision, not a default.
var unmappedStoreErrors = map[string]string{
	"ErrKeyLen":         "unlock path — the key never crosses CoreAPI",
	"ErrDecrypt":        "unlock path — the key never crosses CoreAPI",
	"ErrReminderPhrase": "daemon-internal delivery-state validation; modules neither cache reminder presentations nor branch on this verdict",

	"ErrToolCmd": "write-side validation of an allowlist mutation; the caller is the owner at a step-up, not a module branching on the verdict",

	"ErrProposedRoutineNotFound": "no module branches on a routine id that vanished; accept and dismiss are owner clicks",
	"ErrProposedRoutineExists":   "the propose path already reports 'nothing new' through its bool return",
	"ErrRoutineStatus":           "an unknown status is a caller bug, not a state a module recovers from",

	"ErrTaskNotFound": "the task surfaces re-list rather than branch",
	"ErrTaskEmpty":    "input validation — the surface refuses empty text before it gets here",
	"ErrTaskStatus":   "an illegal status move is a caller bug; the surface offers only legal ones",

	// The shopping and packing lists follow the task verdicts exactly, and for
	// the same reasons. No module branches on any of the three today.
	"ErrListItemNotFound": "the surface re-lists rather than branch on an item id that vanished",
	"ErrListItemEmpty":    "input validation — an empty item is refused before it reaches the store",
	"ErrListItemStatus":   "an unknown status is a caller bug, not a state a module recovers from",
}

// The mapping itself, through a wrap, because every real caller wraps.
func TestMapErrMapsEveryPair(t *testing.T) {
	for _, p := range mapErrPairs {
		got := mapErr(fmt.Errorf("storeapi: %w", p.from))
		if got != p.wants {
			t.Errorf("mapErr(store.%s) = %v, want %v", p.name, got, p.wants)
		}
	}
}

// Anything mapErr does not recognise passes through untouched. A module that
// cannot branch on an error must still see the original text.
func TestMapErrPassesUnknownThrough(t *testing.T) {
	if mapErr(nil) != nil {
		t.Error("mapErr(nil) must stay nil")
	}
	own := fmt.Errorf("socket closed")
	if got := mapErr(own); got != own {
		t.Errorf("mapErr(%v) = %v, want the same error back", own, got)
	}
}

// The parity half: every exported sentinel in internal/store is either mapped
// or listed with a reason. Read off the source, so a sentinel added in a file
// this package never touches still trips it.
func TestMapErrCoversEveryStoreSentinel(t *testing.T) {
	mapped := map[string]bool{}
	for _, p := range mapErrPairs {
		mapped[p.name] = true
	}

	for _, name := range storeSentinelNames(t) {
		if mapped[name] || unmappedStoreErrors[name] != "" {
			continue
		}
		t.Errorf("store.%s is a new sentinel with no verdict: add it to mapErr and mapErrPairs, "+
			"or to unmappedStoreErrors with the reason a module cannot branch on it", name)
	}
}

// storeSentinelNames reads internal/store for exported package-level error
// values: `var ErrX = errors.New(...)`, inside a block or on its own.
func storeSentinelNames(t *testing.T) []string {
	t.Helper()
	fset := token.NewFileSet()
	pkgs, err := parser.ParseDir(fset, "../store", func(fi fs.FileInfo) bool {
		return !strings.HasSuffix(fi.Name(), "_test.go")
	}, 0)
	if err != nil {
		t.Fatalf("parse internal/store: %v", err)
	}
	var out []string
	for _, pkg := range pkgs {
		for _, f := range pkg.Files {
			for _, d := range f.Decls {
				gd, ok := d.(*ast.GenDecl)
				if !ok || gd.Tok != token.VAR {
					continue
				}
				for _, spec := range gd.Specs {
					vs, ok := spec.(*ast.ValueSpec)
					if !ok {
						continue
					}
					for i, n := range vs.Names {
						if !strings.HasPrefix(n.Name, "Err") || !n.IsExported() {
							continue
						}
						if i < len(vs.Values) && isErrorsNew(vs.Values[i]) {
							out = append(out, n.Name)
						}
					}
				}
			}
		}
	}
	if len(out) < len(mapErrPairs) {
		t.Fatalf("found %d sentinels in internal/store, fewer than the %d already mapped — the scan is broken, not the store", len(out), len(mapErrPairs))
	}
	return out
}

func isErrorsNew(e ast.Expr) bool {
	call, ok := e.(*ast.CallExpr)
	if !ok {
		return false
	}
	sel, ok := call.Fun.(*ast.SelectorExpr)
	if !ok || sel.Sel.Name != "New" {
		return false
	}
	id, ok := sel.X.(*ast.Ident)
	return ok && id.Name == "errors"
}
