package tools

import (
	"encoding/json"
	"strings"
	"testing"
)

// expectedToolNames is the authoritative tool list: the 14 read-only
// tools from plan §4.3 plus the 5 write tools from plan §3.8 (P3-T4/T5)
// plus the whoami identity tool.
var expectedToolNames = []string{
	"search_assets",
	"get_asset",
	"list_ports",
	"list_high_risk_ports",
	"list_dns_records",
	"list_domains",
	"get_relationship_graph",
	"list_cloud_accounts",
	"list_sync_jobs",
	"list_api_route_assets",
	"search_vulnerabilities",
	"get_vulnerability",
	"list_vuln_lookup_options",
	"get_vuln_tags_stats",
	"submit_vulnerability",
	"update_vulnerability",
	"transition_vulnerability",
	"add_vulnerability_comment",
	"upload_vulnerability_attachment",
	"whoami",
}

// swaggerExemptPaths lists tool endpoints that are deliberately absent
// from the embedded swagger snapshot (it documents only the caasm/vuln
// open surface). The whoami identity endpoint lives on the agent
// self-service surface; its contract is pinned by the backend handler
// tests instead of the registry-level swagger checks below.
var swaggerExemptPaths = map[string]bool{
	"/api/v1/agent/whoami": true,
}

// TestRegistryToolCount asserts the registry holds exactly the 20 tools
// (14 read-only from plan §4.3 + 5 write tools from plan §3.8 + the
// whoami identity tool), uniquely named.
func TestRegistryToolCount(t *testing.T) {
	reg := Registry()
	if len(reg) != 20 {
		t.Fatalf("Registry() returned %d tools, want 20", len(reg))
	}

	seen := make(map[string]bool, len(reg))
	for _, tool := range reg {
		if seen[tool.Name] {
			t.Errorf("duplicate tool name %q", tool.Name)
		}
		seen[tool.Name] = true
	}
	for _, name := range expectedToolNames {
		if !seen[name] {
			t.Errorf("expected tool %q missing from registry", name)
		}
	}
}

// TestEndpointsExistInSwagger asserts every endpoint referenced by the
// registry exists in the embedded swagger snapshot as an operation of
// the endpoint's own method (get for the read-only tools, post/put for
// the write tools), and that GET endpoints never declare a BodyMap.
func TestEndpointsExistInSwagger(t *testing.T) {
	doc, err := loadSwagger()
	if err != nil {
		t.Fatalf("loadSwagger: %v", err)
	}

	for _, tool := range Registry() {
		if len(tool.Endpoints) == 0 {
			t.Errorf("tool %s: no endpoints registered", tool.Name)
		}
		for _, ep := range tool.Endpoints {
			switch ep.Method {
			case "get", "post", "put":
			default:
				t.Errorf("tool %s: endpoint %s has unsupported method %q", tool.Name, ep.Path, ep.Method)
			}
			if ep.Method == "get" && len(ep.BodyMap) > 0 {
				t.Errorf("tool %s: GET endpoint %s must not declare BodyMap", tool.Name, ep.Path)
			}
			if swaggerExemptPaths[ep.Path] {
				continue
			}
			ops, ok := doc.Paths[ep.Path]
			if !ok {
				t.Errorf("tool %s: path %s not found in swagger paths", tool.Name, ep.Path)
				continue
			}
			if _, ok := ops[ep.Method]; !ok {
				t.Errorf("tool %s: path %s has no %s operation in swagger", tool.Name, ep.Path, ep.Method)
			}
		}
	}
}

// TestParamMapMatchesSwagger asserts every mapped tool argument exists
// in the tool's InputSchema, and every mapped backend parameter exists
// (with a matching type) in the swagger operation's parameters. It also
// asserts every swagger path parameter is covered by the ParamMap, so
// path placeholders (id/public_id) can always be filled.
func TestParamMapMatchesSwagger(t *testing.T) {
	doc, err := loadSwagger()
	if err != nil {
		t.Fatalf("loadSwagger: %v", err)
	}

	for _, tool := range Registry() {
		if tool.InputSchema == nil {
			t.Errorf("tool %s: nil InputSchema", tool.Name)
			continue
		}
		for _, ep := range tool.Endpoints {
			if swaggerExemptPaths[ep.Path] {
				continue
			}
			op, ok := doc.Paths[ep.Path][ep.Method]
			if !ok {
				t.Errorf("tool %s: %s %s operation missing, skipping param checks", tool.Name, ep.Path, ep.Method)
				continue
			}

			for toolArg, backendParam := range ep.ParamMap {
				prop, ok := tool.InputSchema.Properties[toolArg]
				if !ok {
					t.Errorf("tool %s endpoint %s: ParamMap references tool argument %q absent from InputSchema",
						tool.Name, ep.Path, toolArg)
					continue
				}
				sp, ok := op.param(backendParam)
				if !ok {
					t.Errorf("tool %s endpoint %s: backend parameter %q not declared in swagger operation",
						tool.Name, ep.Path, backendParam)
					continue
				}
				if prop.Type != sp.Type {
					t.Errorf("tool %s endpoint %s: parameter %q type %q, swagger declares %q",
						tool.Name, ep.Path, backendParam, prop.Type, sp.Type)
				}
			}

			for _, sp := range op.Parameters {
				if sp.In != "path" {
					continue
				}
				mapped := false
				for _, backendParam := range ep.ParamMap {
					if backendParam == sp.Name {
						mapped = true
						break
					}
				}
				if !mapped {
					t.Errorf("tool %s endpoint %s: swagger path parameter %q not covered by ParamMap",
						tool.Name, ep.Path, sp.Name)
				}
			}
		}
	}
}

// TestBodyMapShape asserts the structural sanity of every endpoint's
// BodyMap: GET endpoints never declare one, BodyMap and ParamMap are
// disjoint (one argument goes to exactly one place), and every mapped
// tool argument exists in the tool's InputSchema. Field-level
// conformance to the swagger request body is checked at the P3-T6 gate.
func TestBodyMapShape(t *testing.T) {
	for _, tool := range Registry() {
		for _, ep := range tool.Endpoints {
			for argName, field := range ep.BodyMap {
				if ep.Method == "get" {
					t.Errorf("tool %s endpoint %s: GET endpoint must not declare BodyMap (%q → %q)",
						tool.Name, ep.Path, argName, field)
				}
				if field == "" {
					t.Errorf("tool %s endpoint %s: BodyMap argument %q has empty body field name",
						tool.Name, ep.Path, argName)
				}
				if _, ok := ep.ParamMap[argName]; ok {
					t.Errorf("tool %s endpoint %s: argument %q mapped by both ParamMap and BodyMap",
						tool.Name, ep.Path, argName)
				}
				if tool.InputSchema == nil {
					t.Errorf("tool %s: nil InputSchema", tool.Name)
					continue
				}
				if _, ok := tool.InputSchema.Properties[argName]; !ok {
					t.Errorf("tool %s endpoint %s: BodyMap references tool argument %q absent from InputSchema",
						tool.Name, ep.Path, argName)
				}
			}
		}
	}
}

// TestInputSchemaShape asserts the structural sanity of every tool
// schema: object type, non-empty properties map, required ⊆ properties,
// and every property fully described.
func TestInputSchemaShape(t *testing.T) {
	for _, tool := range Registry() {
		s := tool.InputSchema
		if s == nil {
			t.Errorf("tool %s: nil InputSchema", tool.Name)
			continue
		}
		if s.Type != "object" {
			t.Errorf("tool %s: schema type %q, want \"object\"", tool.Name, s.Type)
		}
		if s.Properties == nil {
			t.Errorf("tool %s: nil Properties (want empty object, not null)", tool.Name)
		}
		for _, req := range s.Required {
			if _, ok := s.Properties[req]; !ok {
				t.Errorf("tool %s: required argument %q absent from properties", tool.Name, req)
			}
		}
		for name, prop := range s.Properties {
			if prop.Type == "" {
				t.Errorf("tool %s: property %q has empty type", tool.Name, name)
			}
			if prop.Description == "" {
				t.Errorf("tool %s: property %q has empty description", tool.Name, name)
			}
		}
		if tool.Description == "" {
			t.Errorf("tool %s: empty description", tool.Name)
		}
	}
}

// TestListOutputShape asserts List() returns the MCP-style tools/list
// structure for all 20 tools.
func TestListOutputShape(t *testing.T) {
	res := List()
	if len(res.Tools) != 20 {
		t.Fatalf("List() returned %d tools, want 20", len(res.Tools))
	}

	raw, err := json.Marshal(res)
	if err != nil {
		t.Fatalf("marshal List(): %v", err)
	}
	var decoded struct {
		Tools []struct {
			Name        string          `json:"name"`
			Description string          `json:"description"`
			InputSchema json.RawMessage `json:"inputSchema"`
		} `json:"tools"`
	}
	if err := json.Unmarshal(raw, &decoded); err != nil {
		t.Fatalf("unmarshal List() output: %v", err)
	}
	for _, entry := range decoded.Tools {
		if entry.Name == "" || entry.Description == "" {
			t.Errorf("List() entry with empty name/description: %+v", entry)
		}
		var schema struct {
			Type string `json:"type"`
		}
		if err := json.Unmarshal(entry.InputSchema, &schema); err != nil {
			t.Errorf("tool %s: invalid inputSchema JSON: %v", entry.Name, err)
			continue
		}
		if schema.Type != "object" {
			t.Errorf("tool %s: inputSchema.type %q, want \"object\"", entry.Name, schema.Type)
		}
	}
	if !strings.HasPrefix(string(raw), `{"tools":[`) {
		t.Errorf("List() output does not start with the MCP tools array envelope")
	}
}
