package contracts

import (
	"strings"
	"testing"

	"github.com/zzet/gortex/internal/graph"
)

// Each test builds a minimal source file with a route and a handler,
// asks the extractor for contracts, and asserts on the schema-shape
// Meta keys. We only check the enriched fields — framework-routing
// coverage lives in http_test.go.

func TestHTTPEnrich_Go_NetHTTP_Stdlib_FullSchema(t *testing.T) {
	src := []byte(`package api

import (
	"encoding/json"
	"net/http"
)

type CreateReq struct {
	Name string ` + "`json:\"name\"`" + `
}

type CreateResp struct {
	ID string ` + "`json:\"id\"`" + `
}

func register(mux *http.ServeMux) {
	mux.HandleFunc("POST /users", createUser)
}

func createUser(w http.ResponseWriter, r *http.Request) {
	var req CreateReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	name := r.URL.Query().Get("tenant")
	_ = name
	resp := CreateResp{ID: "x"}
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(resp)
}
`)

	nodes := []*graph.Node{
		{ID: "pkg/api.go::register", Name: "register", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 15, EndLine: 17},
		{ID: "pkg/api.go::createUser", Name: "createUser", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 19, EndLine: 30},
		{ID: "pkg/api.go::CreateReq", Name: "CreateReq", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 7, EndLine: 9},
		{ID: "pkg/api.go::CreateResp", Name: "CreateResp", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 11, EndLine: 13},
	}
	cs := (&HTTPExtractor{}).Extract("pkg/api.go", src, nodes, nil)

	c := findContract(t, cs, "http::POST::/users", RoleProvider)
	assertMetaString(t, c, "request_type", "pkg/api.go::CreateReq")
	assertMetaString(t, c, "response_type", "pkg/api.go::CreateResp")
	assertMetaStrings(t, c, "query_params", []string{"tenant"})
	assertMetaInts(t, c, "status_codes", []int{201, 400})
	assertMetaString(t, c, "schema_source", "extracted")
}

func TestHTTPEnrich_Go_Gin_RequestAndResponse(t *testing.T) {
	src := []byte(`package api

import "github.com/gin-gonic/gin"

type LoginReq struct{ Email string }
type LoginResp struct{ Token string }

func register(r *gin.Engine) {
	r.POST("/login", login)
}

func login(c *gin.Context) {
	var req LoginReq
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	resp := LoginResp{Token: "t"}
	c.JSON(200, resp)
}
`)
	nodes := []*graph.Node{
		{ID: "pkg/api.go::register", Name: "register", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 7, EndLine: 9},
		{ID: "pkg/api.go::login", Name: "login", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 11, EndLine: 20},
		{ID: "pkg/api.go::LoginReq", Name: "LoginReq", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 5, EndLine: 5},
		{ID: "pkg/api.go::LoginResp", Name: "LoginResp", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 6, EndLine: 6},
	}
	cs := (&HTTPExtractor{}).Extract("pkg/api.go", src, nodes, nil)

	c := findContract(t, cs, "http::POST::/login", RoleProvider)
	assertMetaString(t, c, "request_type", "pkg/api.go::LoginReq")
	assertMetaString(t, c, "response_type", "pkg/api.go::LoginResp")
	assertMetaInts(t, c, "status_codes", []int{200, 400})
	assertMetaString(t, c, "schema_source", "extracted")
}

func TestHTTPEnrich_Go_Fiber_Binding(t *testing.T) {
	src := []byte(`package api

import "github.com/gofiber/fiber/v2"

type CreateTuckReq struct{ Body string }

func register(app *fiber.App) {
	app.POST("/v1/tucks", createTuck)
}

func createTuck(c *fiber.Ctx) error {
	var req CreateTuckReq
	if err := c.BodyParser(&req); err != nil {
		return c.Status(400).JSON(fiber.Map{"error": err.Error()})
	}
	limit := c.Query("limit")
	_ = limit
	return c.JSON(req)
}
`)
	nodes := []*graph.Node{
		{ID: "pkg/api.go::register", Name: "register", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 7, EndLine: 9},
		{ID: "pkg/api.go::createTuck", Name: "createTuck", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 11, EndLine: 19},
		{ID: "pkg/api.go::CreateTuckReq", Name: "CreateTuckReq", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 5, EndLine: 5},
	}
	cs := (&HTTPExtractor{}).Extract("pkg/api.go", src, nodes, nil)

	c := findContract(t, cs, "http::POST::/v1/tucks", RoleProvider)
	assertMetaString(t, c, "request_type", "pkg/api.go::CreateTuckReq")
	assertMetaStrings(t, c, "query_params", []string{"limit"})
	assertMetaInts(t, c, "status_codes", []int{400})
	// response is the same type as request in this fixture.
	assertMetaString(t, c, "schema_source", "extracted")
}

// Map-envelope response recursion still resolves when the inner
// identifier has a syntactic type we can read (Stage 1 scope).
// Cases where the inner identifier comes from a method-call return
// (e.g. `workspaces, err := h.svc.List(...)`) belong to the
// graph-aware `resolveCallReturnTypes` post-pass covered in the
// indexer test suite.
func TestHTTPEnrich_Go_RespondJSONEnvelope_SyntacticInner(t *testing.T) {
	src := []byte(`package api

import "net/http"

type Workspace struct { ID string }

func register(mux *http.ServeMux) {
	mux.HandleFunc("GET /v1/workspaces", h.ListWorkspaces)
}

func (h *Handler) ListWorkspaces(w http.ResponseWriter, r *http.Request) {
	ws := &Workspace{}
	respondJSON(w, http.StatusOK, map[string]interface{}{"data": ws})
}
`)
	nodes := []*graph.Node{
		{ID: "pkg/api.go::register", Name: "register", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 7, EndLine: 9},
		{ID: "pkg/api.go::Handler.ListWorkspaces", Name: "ListWorkspaces", Kind: graph.KindMethod, FilePath: "pkg/api.go", StartLine: 11, EndLine: 14},
		{ID: "pkg/api.go::Workspace", Name: "Workspace", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 5, EndLine: 5},
	}
	cs := (&HTTPExtractor{}).Extract("pkg/api.go", src, nodes, nil)
	c := findContract(t, cs, "http::GET::/v1/workspaces", RoleProvider)

	assertMetaString(t, c, "response_type", "pkg/api.go::Workspace")
	assertMetaInts(t, c, "status_codes", []int{200})
	assertMetaString(t, c, "schema_source", "extracted")
}

// Multi-key envelope: every key surfaces on response_envelope, with
// the source expression preserved per row. The keys whose values
// resolve to a typed binding get a "type"; the rest carry just the
// expression, which is still vastly more useful than the raw
// WriteJSON call previously stamped onto response_expr.
// Regression: a value like `[]any{evt1, evt2}` used to be captured
// by the multi-key envelope regex as `[]any{` (truncated at the
// first inner `}`), producing a malformed expr field in the
// dashboard. The brace/bracket-balanced splitter keeps the value
// expression intact and recognises the slice composite literal.
func TestHTTPEnrich_Go_EnvelopeValueIsNestedSliceLiteral(t *testing.T) {
	src := []byte(`package api

import "net/http"

type Event struct{ Name string }

func register(mux *http.ServeMux) {
	mux.HandleFunc("GET /v1/activity", logActivity)
}

func logActivity(w http.ResponseWriter, r *http.Request) {
	WriteJSON(w, http.StatusOK, map[string]any{"events": []any{Event{}, Event{}}})
}

func WriteJSON(w http.ResponseWriter, code int, body any) {}
`)
	nodes := []*graph.Node{
		{ID: "pkg/api.go::register", Name: "register", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 7, EndLine: 9},
		{ID: "pkg/api.go::logActivity", Name: "logActivity", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 11, EndLine: 13},
		{ID: "pkg/api.go::Event", Name: "Event", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 5, EndLine: 5},
	}
	cs := (&HTTPExtractor{}).Extract("pkg/api.go", src, nodes, nil)
	c := findContract(t, cs, "http::GET::/v1/activity", RoleProvider)

	env, ok := c.Meta["response_envelope"].([]map[string]any)
	if !ok || len(env) != 1 {
		t.Fatalf("response_envelope = %#v, want one row", c.Meta["response_envelope"])
	}
	row := env[0]
	expr, _ := row["expr"].(string)
	if expr == "" || strings.HasSuffix(expr, "{") {
		t.Errorf("expr is truncated: %q", expr)
	}
	if !strings.Contains(expr, "[]any{") || !strings.HasSuffix(expr, "}") {
		t.Errorf("expr missing the full literal; got %q", expr)
	}
	if got, _ := row["type"].(string); got == "" {
		t.Errorf("type empty for inline slice literal; want a recognised element type")
	}
	if r, _ := row["repeated"].(bool); !r {
		t.Errorf("repeated=false; want true for []any{...}")
	}
}

func TestHTTPEnrich_Go_RespondJSONEnvelope_MultiKey(t *testing.T) {
	src := []byte(`package api

import "net/http"

type FileEntry struct{ Path string }

func register(mux *http.ServeMux) {
	mux.HandleFunc("GET /v1/overlay/sessions/{id}/files", h.handleOverlayList)
}

func (h *Handler) handleOverlayList(w http.ResponseWriter, r *http.Request) {
	out := []FileEntry{}
	total := 0
	WriteJSON(w, http.StatusOK, map[string]any{"files": out, "total": total})
}
`)
	nodes := []*graph.Node{
		{ID: "pkg/api.go::register", Name: "register", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 7, EndLine: 9},
		{ID: "pkg/api.go::Handler.handleOverlayList", Name: "handleOverlayList", Kind: graph.KindMethod, FilePath: "pkg/api.go", StartLine: 11, EndLine: 15},
		{ID: "pkg/api.go::FileEntry", Name: "FileEntry", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 5, EndLine: 5},
	}
	cs := (&HTTPExtractor{}).Extract("pkg/api.go", src, nodes, nil)
	c := findContract(t, cs, "http::GET::/v1/overlay/sessions/{p1}/files", RoleProvider)

	env, ok := c.Meta["response_envelope"].([]map[string]any)
	if !ok || len(env) != 2 {
		t.Fatalf("response_envelope = %#v, want 2 rows", c.Meta["response_envelope"])
	}
	byName := map[string]map[string]any{}
	for _, row := range env {
		name, _ := row["name"].(string)
		byName[name] = row
	}
	if files := byName["files"]; files == nil {
		t.Errorf("envelope missing 'files' row; got %v", byName)
	} else if expr, _ := files["expr"].(string); expr != "out" {
		t.Errorf("envelope['files'].expr = %q, want \"out\"", expr)
	}
	if total := byName["total"]; total == nil {
		t.Errorf("envelope missing 'total' row; got %v", byName)
	} else if expr, _ := total["expr"].(string); expr != "total" {
		t.Errorf("envelope['total'].expr = %q, want \"total\"", expr)
	}
	// The previous behavior stamped the entire WriteJSON(...) call
	// as response_expr. With the envelope present we keep just the
	// value literal so the raw view is short.
	if got, _ := c.Meta["response_expr"].(string); strings.HasPrefix(got, "WriteJSON(") {
		t.Errorf("response_expr leaks the full WriteJSON wrapper: %q", got)
	}
}

func TestHTTPEnrich_Go_PathParams_AlwaysPresent(t *testing.T) {
	src := []byte(`package api

import "net/http"

func register(mux *http.ServeMux) {
	mux.HandleFunc("GET /v1/workspaces/{wid}/tags/{id}", getTag)
}

func getTag(w http.ResponseWriter, r *http.Request) {}
`)
	nodes := []*graph.Node{
		{ID: "pkg/api.go::register", Name: "register", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 5, EndLine: 7},
		{ID: "pkg/api.go::getTag", Name: "getTag", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 9, EndLine: 9},
	}
	cs := (&HTTPExtractor{}).Extract("pkg/api.go", src, nodes, nil)

	c := findContract(t, cs, "http::GET::/v1/workspaces/{p1}/tags/{p2}", RoleProvider)
	// path_params are derived from the normalised template (positional
	// names). The user-written {wid} / {id} slots are rewritten to
	// {p1} / {p2} so cross-repo contract IDs match even when provider
	// and consumer teams picked different names.
	assertMetaStrings(t, c, "path_params", []string{"p1", "p2"})
}

func TestHTTPEnrich_Go_UnresolvedType_PartialSchema(t *testing.T) {
	src := []byte(`package api

import (
	"encoding/json"
	"net/http"
)

func register(mux *http.ServeMux) {
	mux.HandleFunc("POST /x", createX)
}

func createX(w http.ResponseWriter, r *http.Request) {
	var req UnknownThing
	_ = json.NewDecoder(r.Body).Decode(&req)
}
`)
	nodes := []*graph.Node{
		{ID: "pkg/api.go::register", Name: "register", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 8, EndLine: 10},
		{ID: "pkg/api.go::createX", Name: "createX", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 12, EndLine: 15},
	}
	cs := (&HTTPExtractor{}).Extract("pkg/api.go", src, nodes, nil)

	c := findContract(t, cs, "http::POST::/x", RoleProvider)
	// Type name wasn't in the file's node list, so we keep the bare
	// name and flag the meta so a later module-wide pass can resolve
	// it.
	assertMetaString(t, c, "request_type", "UnknownThing")
	assertMetaString(t, c, "schema_source", "extracted")
}

func TestHTTPEnrich_Go_Consumer_MarshalAndDecode(t *testing.T) {
	src := []byte(`package api

import (
	"bytes"
	"encoding/json"
	"net/http"
)

type CreateTuckReq struct{ Title string }
type CreateTuckResp struct{ ID string }

func callCreate() error {
	req := CreateTuckReq{Title: "a"}
	data, _ := json.Marshal(req)
	r, err := http.NewRequest("POST", "http://api/v1/tucks", bytes.NewReader(data))
	if err != nil {
		return err
	}
	resp, err := http.DefaultClient.Do(r)
	if err != nil {
		return err
	}
	var out CreateTuckResp
	_ = json.NewDecoder(resp.Body).Decode(&out)
	return nil
}
`)
	nodes := []*graph.Node{
		{ID: "pkg/api.go::callCreate", Name: "callCreate", Kind: graph.KindFunction, FilePath: "pkg/api.go", StartLine: 11, EndLine: 25},
		{ID: "pkg/api.go::CreateTuckReq", Name: "CreateTuckReq", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 9, EndLine: 9},
		{ID: "pkg/api.go::CreateTuckResp", Name: "CreateTuckResp", Kind: graph.KindType, FilePath: "pkg/api.go", StartLine: 10, EndLine: 10},
	}
	cs := (&HTTPExtractor{}).Extract("pkg/api.go", src, nodes, nil)

	c := findContract(t, cs, "http::POST::/v1/tucks", RoleConsumer)
	assertMetaString(t, c, "request_type", "pkg/api.go::CreateTuckReq")
	assertMetaString(t, c, "response_type", "pkg/api.go::CreateTuckResp")
	assertMetaString(t, c, "schema_source", "extracted")
}

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

func findContract(t *testing.T, cs []Contract, id string, role Role) Contract {
	t.Helper()
	for _, c := range cs {
		if c.ID == id && c.Role == role {
			return c
		}
	}
	ids := make([]string, 0, len(cs))
	for _, c := range cs {
		ids = append(ids, string(c.Role)+" "+c.ID)
	}
	t.Fatalf("contract %s (%s) not found. have: %v", id, role, ids)
	return Contract{}
}

func assertMetaString(t *testing.T, c Contract, key, want string) {
	t.Helper()
	got, ok := c.Meta[key].(string)
	if !ok {
		t.Errorf("meta[%q] missing or not string on %s (got %T = %v)", key, c.ID, c.Meta[key], c.Meta[key])
		return
	}
	if got != want {
		t.Errorf("meta[%q] on %s = %q, want %q", key, c.ID, got, want)
	}
}

func assertMetaStrings(t *testing.T, c Contract, key string, want []string) {
	t.Helper()
	got, ok := c.Meta[key].([]string)
	if !ok {
		t.Errorf("meta[%q] missing or wrong type on %s: %T", key, c.ID, c.Meta[key])
		return
	}
	if len(got) != len(want) {
		t.Errorf("meta[%q] on %s = %v, want %v", key, c.ID, got, want)
		return
	}
	for i := range got {
		if got[i] != want[i] {
			t.Errorf("meta[%q] on %s = %v, want %v", key, c.ID, got, want)
			return
		}
	}
}

func assertMetaInts(t *testing.T, c Contract, key string, want []int) {
	t.Helper()
	got, ok := c.Meta[key].([]int)
	if !ok {
		t.Errorf("meta[%q] missing or wrong type on %s: %T", key, c.ID, c.Meta[key])
		return
	}
	if len(got) != len(want) {
		t.Errorf("meta[%q] on %s = %v, want %v", key, c.ID, got, want)
		return
	}
	for i := range got {
		if got[i] != want[i] {
			t.Errorf("meta[%q] on %s = %v, want %v", key, c.ID, got, want)
			return
		}
	}
}
