package tools

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strings"
	"testing"
)

// writeToolExpectations pins the contract of the four P3-T4 write
// tools: endpoint method/path and the body fields deliberately NOT
// exposed (server-forced or agent-403 fields, plus the W0 tags cut).
var writeToolExpectations = map[string]struct {
	method   string
	path     string
	excluded []string
}{
	"submit_vulnerability": {
		method: "post",
		path:   "/api/v1/vulnerabilities",
		// 服务端强制（提交人=Key 主人、时间字段）+ tags 首版裁剪。
		excluded: []string{"tags", "submitter_user_id", "submitted_at", "sla_deadline"},
	},
	"update_vulnerability": {
		method: "put",
		path:   "/api/v1/vulnerabilities/{public_id}",
		// agent 触碰即整体 403 的五个字段 + tags 首版裁剪。
		excluded: []string{"tags", "poc_text", "verification_steps", "has_poc_verification", "submitted_at", "sla_deadline"},
	},
	"transition_vulnerability": {
		method: "post",
		path:   "/api/v1/vulnerabilities/{public_id}/transition",
	},
	"add_vulnerability_comment": {
		method: "post",
		path:   "/api/v1/vulnerabilities/{public_id}/comments",
	},
}

// swaggerBodyDoc is the slice of the swagger snapshot the write-tool
// tests validate against: operations' in-body parameter $ref plus the
// referenced definitions (required list and property types).
type swaggerBodyDoc struct {
	Paths map[string]map[string]struct {
		Parameters []struct {
			Name   string `json:"name"`
			In     string `json:"in"`
			Schema struct {
				Ref string `json:"$ref"`
			} `json:"schema"`
		} `json:"parameters"`
	} `json:"paths"`
	Definitions map[string]struct {
		Type       string   `json:"type"`
		Required   []string `json:"required"`
		Properties map[string]struct {
			Type string `json:"type"`
			Ref  string `json:"$ref"`
		} `json:"properties"`
	} `json:"definitions"`
}

func loadSwaggerBodyDoc(t *testing.T) *swaggerBodyDoc {
	t.Helper()
	var doc swaggerBodyDoc
	if err := json.Unmarshal(swaggerJSON, &doc); err != nil {
		t.Fatalf("parse embedded swagger.json: %v", err)
	}
	return &doc
}

// bodyContract resolves the request body definition of the given
// operation and returns its required list and property name → type
// map (one $ref hop resolved, for the string enum types).
func bodyContract(t *testing.T, doc *swaggerBodyDoc, path, method string) (required []string, props map[string]string) {
	t.Helper()
	ops, ok := doc.Paths[path]
	if !ok {
		t.Fatalf("swagger: path %s not found", path)
	}
	op, ok := ops[method]
	if !ok {
		t.Fatalf("swagger: path %s has no %s operation", path, method)
	}
	ref := ""
	for _, p := range op.Parameters {
		if p.In == "body" {
			ref = p.Schema.Ref
		}
	}
	if ref == "" {
		t.Fatalf("swagger: %s %s declares no in-body parameter", method, path)
	}
	name := strings.TrimPrefix(ref, "#/definitions/")
	def, ok := doc.Definitions[name]
	if !ok {
		t.Fatalf("swagger: definition %s not found", name)
	}
	props = make(map[string]string, len(def.Properties))
	for fname, fdef := range def.Properties {
		typ := fdef.Type
		if typ == "" && fdef.Ref != "" {
			typ = doc.Definitions[strings.TrimPrefix(fdef.Ref, "#/definitions/")].Type
		}
		props[fname] = typ
	}
	return def.Required, props
}

func findWriteTool(t *testing.T, name string) Tool {
	t.Helper()
	tool, ok := findTool(name)
	if !ok {
		t.Fatalf("tool %q not registered", name)
	}
	return tool
}

// TestWriteToolsRegistered asserts the four write tools are registered
// with exactly one endpoint each, on the contract's method/path, and
// that required argument lists and enums match the contract.
func TestWriteToolsRegistered(t *testing.T) {
	for name, want := range writeToolExpectations {
		tool := findWriteTool(t, name)
		if len(tool.Endpoints) != 1 {
			t.Fatalf("tool %s: want exactly 1 endpoint, got %d", name, len(tool.Endpoints))
		}
		ep := tool.Endpoints[0]
		if ep.Method != want.method || ep.Path != want.path {
			t.Errorf("tool %s: endpoint = %s %s, want %s %s", name, ep.Method, ep.Path, want.method, want.path)
		}
	}

	// Required argument lists (public_id 是路径参数，其余与契约必填一致)。
	wantRequired := map[string][]string{
		"submit_vulnerability":      {"title", "severity_level", "status", "discoverer_employee_id"},
		"update_vulnerability":      {"public_id"},
		"transition_vulnerability":  {"public_id", "status"},
		"add_vulnerability_comment": {"public_id", "content"},
	}
	for name, want := range wantRequired {
		tool := findWriteTool(t, name)
		if len(tool.InputSchema.Required) != len(want) {
			t.Fatalf("tool %s: required = %v, want %v", name, tool.InputSchema.Required, want)
		}
		for _, req := range want {
			found := false
			for _, got := range tool.InputSchema.Required {
				if got == req {
					found = true
				}
			}
			if !found {
				t.Errorf("tool %s: required argument %q missing (got %v)", name, req, tool.InputSchema.Required)
			}
		}
	}

	// transition 的 status 枚举 = 五个目标状态（契约字段名是 status
	// 而非 action，对应 request_retest/mark_fixed/... 五个动作）。
	transition := findWriteTool(t, "transition_vulnerability")
	wantEnum := []string{"retesting", "fixed", "false_positive", "risk_accepted", "active"}
	got := transition.InputSchema.Properties["status"].Enum
	if len(got) != len(wantEnum) {
		t.Fatalf("transition status enum = %v, want %v", got, wantEnum)
	}
	for i, v := range wantEnum {
		if got[i] != v {
			t.Errorf("transition status enum[%d] = %q, want %q", i, got[i], v)
		}
	}
}

// TestWriteToolBodyMapMatchesSwagger asserts every write tool's BodyMap
// targets real swagger request-body fields with matching types, that
// every swagger-required body field is mapped, and that the
// deliberately cut fields stay unmapped and undeclared.
func TestWriteToolBodyMapMatchesSwagger(t *testing.T) {
	doc := loadSwaggerBodyDoc(t)

	for name, want := range writeToolExpectations {
		tool := findWriteTool(t, name)
		ep := tool.Endpoints[0]
		required, props := bodyContract(t, doc, ep.Path, ep.Method)

		for arg, field := range ep.BodyMap {
			typ, ok := props[field]
			if !ok {
				t.Errorf("tool %s: BodyMap field %q (arg %q) not in swagger request body", name, field, arg)
				continue
			}
			if got := tool.InputSchema.Properties[arg].Type; got != typ {
				t.Errorf("tool %s: arg %q type %q, swagger body field %q is %q", name, arg, got, field, typ)
			}
		}

		for _, req := range required {
			mapped := false
			for _, field := range ep.BodyMap {
				if field == req {
					mapped = true
				}
			}
			if !mapped {
				t.Errorf("tool %s: swagger-required body field %q has no argument mapping", name, req)
			}
		}

		for _, cut := range want.excluded {
			if _, ok := props[cut]; !ok {
				t.Fatalf("tool %s: excluded field %q not in swagger body — contract changed, re-check the cut", name, cut)
			}
			for arg, field := range ep.BodyMap {
				if field == cut {
					t.Errorf("tool %s: cut field %q must stay unmapped (arg %q)", name, cut, arg)
				}
			}
			if _, ok := tool.InputSchema.Properties[cut]; ok {
				t.Errorf("tool %s: cut field %q must stay undeclared in InputSchema", name, cut)
			}
		}
	}
}

// TestWriteToolDescriptionsDocumentConstraints asserts the LLM-facing
// descriptions carry the key constraint wording (P3-T4 验收③).
func TestWriteToolDescriptionsDocumentConstraints(t *testing.T) {
	cases := map[string][]string{
		"submit_vulnerability": {
			"暂不支持 tags",
			"attachment_id",
			"API Key 主人",           // submitter 强制 = Key 主人
			"服务端强制",                // submitted_at/sla_deadline
			"vuln:submit_findings", // 提交漏洞的新权限码
			"无法配置该 scope",          // 无安全权限的用户/研发的 Key
			"403",                  // 无 scope 调用将被后端 403
		},
		"update_vulnerability": {
			"403",
			"poc_text", "verification_steps", "has_poc_verification", "submitted_at", "sla_deadline",
			"暂不支持 tags",
			"transition_vulnerability",
		},
		"transition_vulnerability": {
			"request_retest", "mark_fixed", "mark_false_positive", "mark_risk_accepted", "reopen",
			"修复人", // request_retest 的修复人前置约束
			"vuln:request_retest", "vuln:close_findings",
			"retesting", "fixed", "false_positive", "risk_accepted", "active",
		},
		"add_vulnerability_comment": {
			"提交人", "修复人",
		},
	}
	for name, substrings := range cases {
		desc := findWriteTool(t, name).Description
		for _, sub := range substrings {
			if !strings.Contains(desc, sub) {
				t.Errorf("tool %s: description missing constraint wording %q", name, sub)
			}
		}
	}
}

// vulnWriteCall captures the single backend request a write tool makes.
type vulnWriteCall struct {
	Method      string
	Path        string
	ContentType string
	Body        map[string]any
}

// callWriteTool dispatches one write tool against a mock backend that
// records method/path/Content-Type/JSON body and replies 200 OK.
func callWriteTool(t *testing.T, name string, args map[string]any) (any, vulnWriteCall) {
	t.Helper()
	var call vulnWriteCall
	c, _ := newVulnMockBackend(t, func(w http.ResponseWriter, r *http.Request) {
		call.Method, call.Path = r.Method, r.URL.Path
		call.ContentType = r.Header.Get("Content-Type")
		_ = json.NewDecoder(r.Body).Decode(&call.Body)
		writeVulnEnvelope(t, w, 0, "ok", map[string]any{"public_id": "VULN-2024-0001"})
	})
	data, handled, err := dispatchVuln(context.Background(), c, name, args)
	if err != nil || !handled {
		t.Fatalf("%s: expected (data,true,nil), got handled=%v err=%v", name, handled, err)
	}
	return data, call
}

func TestSubmitVulnerabilityCall(t *testing.T) {
	data, call := callWriteTool(t, "submit_vulnerability", map[string]any{
		"title":                  "SQL 注入",
		"severity_level":         "high",
		"status":                 "active",
		"discoverer_employee_id": float64(42),
		"fixer_employee_id":      float64(7),
		"affected_assets_count":  float64(3),
		"is_third_party":         false,
		"description":            "<p>详情</p>",
		// 裁剪/服务端强制/未声明的参数一律不得进入 body。
		"tags":              []any{"web"},
		"submitter_user_id": float64(999),
		"sla_deadline":      "2030-01-01T00:00:00Z",
		"page":              float64(2),
	})

	if call.Method != http.MethodPost || call.Path != "/api/v1/vulnerabilities" {
		t.Fatalf("want POST /api/v1/vulnerabilities, got %s %s", call.Method, call.Path)
	}
	if call.ContentType != "application/json" {
		t.Errorf("want Content-Type application/json, got %q", call.ContentType)
	}
	want := map[string]any{
		"title":                  "SQL 注入",
		"severity_level":         "high",
		"status":                 "active",
		"discoverer_employee_id": float64(42),
		"fixer_employee_id":      float64(7),
		"affected_assets_count":  float64(3),
		"is_third_party":         false,
		"description":            "<p>详情</p>",
	}
	for k, v := range want {
		if call.Body[k] != v {
			t.Errorf("body[%q] = %v (%T), want %v", k, call.Body[k], call.Body[k], v)
		}
	}
	for _, k := range []string{"tags", "submitter_user_id", "sla_deadline", "page", "application", "poc_text"} {
		if _, ok := call.Body[k]; ok {
			t.Errorf("body must not contain %q", k)
		}
	}
	m, ok := data.(map[string]any)
	if !ok || m["public_id"] != "VULN-2024-0001" {
		t.Errorf("unexpected response data %v", data)
	}
}

func TestUpdateVulnerabilityCall(t *testing.T) {
	_, call := callWriteTool(t, "update_vulnerability", map[string]any{
		"public_id":      "VULN-2024-0001",
		"title":          "新标题",
		"severity_level": "critical",
		// agent 403 字段与 tags 不得进入 body。
		"poc_text": "rm -rf",
		"tags":     []any{"web"},
	})

	if call.Method != http.MethodPut || call.Path != "/api/v1/vulnerabilities/VULN-2024-0001" {
		t.Fatalf("want PUT /api/v1/vulnerabilities/VULN-2024-0001, got %s %s", call.Method, call.Path)
	}
	if call.Body["title"] != "新标题" || call.Body["severity_level"] != "critical" {
		t.Errorf("unexpected body %v", call.Body)
	}
	for _, k := range []string{"public_id", "poc_text", "tags"} {
		if _, ok := call.Body[k]; ok {
			t.Errorf("body must not contain %q (public_id 走路径，poc_text/tags 不开放)", k)
		}
	}
}

func TestTransitionVulnerabilityCall(t *testing.T) {
	_, call := callWriteTool(t, "transition_vulnerability", map[string]any{
		"public_id": "VULN-2024-0001",
		"status":    "retesting",
		"message":   "已修复，请复测",
	})

	if call.Method != http.MethodPost || call.Path != "/api/v1/vulnerabilities/VULN-2024-0001/transition" {
		t.Fatalf("want POST .../transition, got %s %s", call.Method, call.Path)
	}
	if call.Body["status"] != "retesting" || call.Body["message"] != "已修复，请复测" {
		t.Errorf("unexpected body %v", call.Body)
	}
	if len(call.Body) != 2 {
		t.Errorf("want exactly status+message in body, got %v", call.Body)
	}
}

func TestTransitionVulnerabilityMissingPublicID(t *testing.T) {
	c, reqs := newVulnMockBackend(t, func(w http.ResponseWriter, r *http.Request) {
		t.Errorf("backend must not be called without public_id: %s", r.URL.Path)
	})

	data, handled, err := dispatchVuln(context.Background(), c, "transition_vulnerability", map[string]any{
		"status": "retesting",
	})
	if data != nil || !handled {
		t.Fatalf("expected (nil,true,ArgError), got data=%v handled=%v", data, handled)
	}
	var argErr *ArgError
	if !errors.As(err, &argErr) {
		t.Fatalf("expected *ArgError, got %T (%v)", err, err)
	}
	if !strings.Contains(argErr.Msg, "public_id") {
		t.Errorf("message should mention public_id, got %q", argErr.Msg)
	}
	if got := len(reqs()); got != 0 {
		t.Fatalf("expected no backend requests, got %d", got)
	}
}

func TestAddVulnerabilityCommentCall(t *testing.T) {
	_, call := callWriteTool(t, "add_vulnerability_comment", map[string]any{
		"public_id": "VULN-2024-0001",
		"content":   "复测通过，证据见截图。",
	})

	if call.Method != http.MethodPost || call.Path != "/api/v1/vulnerabilities/VULN-2024-0001/comments" {
		t.Fatalf("want POST .../comments, got %s %s", call.Method, call.Path)
	}
	if call.Body["content"] != "复测通过，证据见截图。" {
		t.Errorf("unexpected body %v", call.Body)
	}
	if len(call.Body) != 1 {
		t.Errorf("want exactly content in body, got %v", call.Body)
	}
}

// TestWriteToolAPIErrorMapping asserts a backend business error on a
// write endpoint (e.g. the agent 403 guard) is returned as the
// structured error payload, same convention as the read tools.
func TestWriteToolAPIErrorMapping(t *testing.T) {
	c, _ := newVulnMockBackend(t, func(w http.ResponseWriter, r *http.Request) {
		writeVulnEnvelope(t, w, 403, "not authorized: agent keys cannot modify poc_text", nil)
	})

	data, handled, err := dispatchVuln(context.Background(), c, "update_vulnerability", map[string]any{
		"public_id": "VULN-2024-0001",
		"title":     "x",
	})
	if err != nil || !handled {
		t.Fatalf("expected (error-payload,true,nil), got handled=%v err=%v", handled, err)
	}
	e := vulnErrPayload(t, data)
	if e["code"] != 403 {
		t.Errorf("want code 403, got %v", e["code"])
	}
	msg, _ := e["message"].(string)
	if !strings.Contains(msg, "poc_text") {
		t.Errorf("want passthrough message naming the guarded field, got %q", msg)
	}
}

// TestWriteToolsDispatchThroughPublicAPI verifies the end-to-end wiring
// of one write tool through call.go's public Dispatch (routing falls
// through CAASM to callVulnTool → dispatchVuln → callEndpoint).
func TestWriteToolsDispatchThroughPublicAPI(t *testing.T) {
	c, reqs := newVulnMockBackend(t, func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || r.URL.Path != "/api/v1/vulnerabilities/VULN-9/comments" {
			t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
		}
		writeVulnEnvelope(t, w, 0, "ok", map[string]any{"id": 1})
	})

	res, err := Dispatch(context.Background(), c, "add_vulnerability_comment", map[string]any{
		"public_id": "VULN-9",
		"content":   "留证",
	})
	if err != nil {
		t.Fatalf("Dispatch returned error: %v", err)
	}
	if _, ok := res.(map[string]any); !ok {
		t.Fatalf("unexpected result %v", res)
	}
	if got := len(reqs()); got != 1 {
		t.Fatalf("expected 1 backend request, got %d", got)
	}
}
