package openapi

import (
	"encoding/json"
	"net/http"
	"reflect"
	"testing"
	"time"
)

// test types exercised by the reflector tests.
type testNested struct {
	Enabled bool   `json:"enabled"`
	Label   string `json:"label,omitempty"`
}

type testPayload struct {
	Name      string            `json:"name"`
	Count     int64             `json:"count"`
	Ratio     float64           `json:"ratio,omitempty"`
	CreatedAt time.Time         `json:"created_at"`
	When      *time.Time        `json:"when,omitempty"`
	Tags      map[string]string `json:"tags,omitempty"`
	Items     []testNested      `json:"items"`
	Raw       json.RawMessage   `json:"raw,omitempty"`
	Data      []byte            `json:"data"`
	Hidden    string            `json:"-"`
	internal  string            // unexported, skipped
}

type testEmbedded struct {
	ID string `json:"id"`
}

type testWithEmbed struct {
	testEmbedded
	Extra string `json:"extra,omitempty"`
}

type testPtrFields struct {
	Size *int64 `json:"size"`
}

func TestReflectNamedStruct(t *testing.T) {
	reg := newTypeRegistry()
	// internal exercises the unexported-field skip path.
	_ = testPayload{internal: "x"}
	s, err := reg.reflectSchema(reflect.TypeOf(testPayload{}))
	if err != nil {
		t.Fatalf("reflectSchema: %v", err)
	}
	if got := s["$ref"]; got != "#/components/schemas/openapi.testPayload" {
		t.Fatalf("unexpected schema: %v", s)
	}
	schema, ok := reg.schemas["openapi.testPayload"]
	if !ok {
		t.Fatalf("testPayload not registered: %v", reg.schemas)
	}
	if schema["type"] != "object" {
		t.Fatalf("type = %v, want object", schema["type"])
	}
	props := schema["properties"].(map[string]any)
	wantRequired := []string{"name", "count", "created_at", "items", "data"}
	gotRequired := schema["required"].([]string)
	if !reflect.DeepEqual(gotRequired, wantRequired) {
		t.Fatalf("required = %v, want %v", gotRequired, wantRequired)
	}
	if _, ok := props["hidden"]; ok {
		t.Fatalf("hidden field must be skipped")
	}
	if _, ok := props["internal"]; ok {
		t.Fatalf("unexported field must be skipped")
	}
	// []byte → string/byte; time.Time → date-time; map → additionalProperties.
	if got := props["data"].(jsonSchema)["format"]; got != "byte" {
		t.Fatalf("data format = %v, want byte", got)
	}
	if got := props["created_at"].(jsonSchema)["format"]; got != "date-time" {
		t.Fatalf("created_at format = %v, want date-time", got)
	}
	ap, ok := props["tags"].(jsonSchema)["additionalProperties"]
	if !ok || ap.(jsonSchema)["type"] != "string" {
		t.Fatalf("tags additionalProperties = %v", props["tags"])
	}
	// items: array of $ref to the nested named type.
	items := props["items"].(jsonSchema)
	if items["type"] != "array" {
		t.Fatalf("items type = %v", items["type"])
	}
	if _, ok := reg.schemas["openapi.testNested"]; !ok {
		t.Fatalf("nested testNested not registered")
	}
	// json.RawMessage → any.
	if got := props["raw"].(jsonSchema); len(got) != 0 {
		t.Fatalf("raw schema = %v, want empty", got)
	}
}

func TestReflectEmbeddedStruct(t *testing.T) {
	reg := newTypeRegistry()
	s, err := reg.reflectSchema(reflect.TypeOf(testWithEmbed{}))
	if err != nil {
		t.Fatalf("reflectSchema: %v", err)
	}
	schema := reg.schemas[s["$ref"].(string)[len("#/components/schemas/"):]]
	props := schema["properties"].(map[string]any)
	if _, ok := props["id"]; !ok {
		t.Fatalf("embedded id not flattened: %v", props)
	}
	required := schema["required"].([]string)
	if !reflect.DeepEqual(required, []string{"id"}) {
		t.Fatalf("required = %v, want [id]", required)
	}
}

func TestReflectPointerFieldsOptional(t *testing.T) {
	reg := newTypeRegistry()
	s, err := reg.reflectSchema(reflect.TypeOf(testPtrFields{}))
	if err != nil {
		t.Fatalf("reflectSchema: %v", err)
	}
	name := s["$ref"].(string)[len("#/components/schemas/"):]
	schema := reg.schemas[name]
	if _, ok := schema["required"]; ok {
		t.Fatalf("pointer field must not be required: %v", schema)
	}
	props := schema["properties"].(map[string]any)
	if got := props["size"].(jsonSchema)["type"]; got != "integer" {
		t.Fatalf("size type = %v, want integer", got)
	}
}

func TestReflectAnonymousStructInlined(t *testing.T) {
	anon := struct {
		A string `json:"a"`
	}{}
	s, err := newTypeRegistry().reflectSchema(reflect.TypeOf(anon))
	if err != nil {
		t.Fatalf("reflectSchema: %v", err)
	}
	if s["type"] != "object" {
		t.Fatalf("anonymous struct must be inlined, got %v", s)
	}
	if _, ok := s["$ref"]; ok {
		t.Fatalf("anonymous struct must not be a $ref: %v", s)
	}
}

func TestReflectCycle(t *testing.T) {
	type cycle struct {
		Self  *cycle            `json:"self,omitempty"`
		Peers []*cycle          `json:"peers,omitempty"`
		Blob  map[string]*cycle `json:"blob,omitempty"`
	}
	reg := newTypeRegistry()
	if _, err := reg.reflectSchema(reflect.TypeOf(cycle{})); err != nil {
		t.Fatalf("reflectSchema: %v", err)
	}
	if _, ok := reg.schemas["openapi.cycle"]; !ok {
		t.Fatalf("cycle type not registered")
	}
}

func TestBuildDocument(t *testing.T) {
	doc := Doc{
		Kind:  ControlPlaneJWT,
		Title: "Test API",
		Security: []map[string][]string{
			{SecurityTiDBCloudJWT: {}},
		},
		Endpoints: []Endpoint{
			{
				Method:  http.MethodPost,
				Path:    "/v1/test/{id}",
				Summary: "Create a test resource",
				Parameters: []Parameter{
					{In: "path", Name: "id", Required: true, Schema: String()},
				},
				RequestBody: &SchemaRef{Type: reflect.TypeOf(testPayload{})},
				Responses: []Response{
					{Code: 201, Description: "Created", Schema: &SchemaRef{Type: reflect.TypeOf(testNested{})}},
				},
				Docs: []DocKind{ControlPlaneJWT},
			},
		},
		OrgIDQuery: true,
	}
	raw, err := Build(doc)
	if err != nil {
		t.Fatalf("Build: %v", err)
	}
	var parsed map[string]any
	if err := json.Unmarshal(raw, &parsed); err != nil {
		t.Fatalf("output is not valid JSON: %v", err)
	}
	if parsed["openapi"] != "3.0.3" {
		t.Fatalf("openapi version = %v", parsed["openapi"])
	}
	op := parsed["paths"].(map[string]any)["/v1/test/{id}"].(map[string]any)["post"].(map[string]any)
	params := op["parameters"].([]any)
	if len(params) != 2 {
		t.Fatalf("expected 2 parameters (path id + orgId), got %d: %v", len(params), params)
	}
	orgParam := params[1].(map[string]any)
	if orgParam["name"] != "orgId" || orgParam["in"] != "query" || orgParam["required"] != true {
		t.Fatalf("orgId param = %v", orgParam)
	}
	requestBody := op["requestBody"].(map[string]any)
	if requestBody["required"] != true {
		t.Fatalf("default requestBody must be required: %v", requestBody)
	}
	if desc, ok := requestBody["description"]; ok && desc != "" {
		t.Fatalf("requestBody description = %v, want empty for empty WithBody desc", desc)
	}
	responses := op["responses"].(map[string]any)
	if _, ok := responses["4XX"]; !ok {
		t.Fatalf("4XX response missing: %v", responses)
	}
	if _, ok := responses["5XX"]; !ok {
		t.Fatalf("5XX response missing: %v", responses)
	}
	schemas := parsed["components"].(map[string]any)["schemas"].(map[string]any)
	if _, ok := schemas["openapi.testPayload"]; !ok {
		t.Fatalf("testPayload schema missing")
	}
	if _, ok := schemas["ErrorResponse"]; !ok {
		t.Fatalf("ErrorResponse must be a registered component")
	}
	// The shared error body must be referenced, not inlined.
	fourXX := responses["4XX"].(map[string]any)
	errorSchema := fourXX["content"].(map[string]any)["application/json"].(map[string]any)["schema"].(map[string]any)
	if errorSchema["$ref"] != "#/components/schemas/ErrorResponse" {
		t.Fatalf("4XX schema = %v, want $ref to ErrorResponse", errorSchema)
	}
	sec := parsed["components"].(map[string]any)["securitySchemes"].(map[string]any)
	if _, ok := sec[SecurityTiDBCloudJWT]; !ok {
		t.Fatalf("JWT scheme missing: %v", sec)
	}
	if _, ok := sec[SecurityDrive9Key]; ok {
		t.Fatalf("unused Drive9Key scheme must be omitted")
	}
}

func TestBuildOptionalRequestBody(t *testing.T) {
	required := false
	doc := Doc{
		Title: "optional body",
		Endpoints: []Endpoint{
			{
				Method:              http.MethodPost,
				Path:                "/v1/opt",
				Summary:             "optional body endpoint",
				RequestBody:         &SchemaRef{Type: reflect.TypeOf(testNested{})},
				requestBodyRequired: &required,
				Docs:                []DocKind{DataPlane},
			},
		},
	}
	raw, err := Build(doc)
	if err != nil {
		t.Fatalf("Build: %v", err)
	}
	var parsed map[string]any
	if err := json.Unmarshal(raw, &parsed); err != nil {
		t.Fatalf("output is not valid JSON: %v", err)
	}
	body := parsed["paths"].(map[string]any)["/v1/opt"].(map[string]any)["post"].(map[string]any)["requestBody"].(map[string]any)
	if body["required"] != false {
		t.Fatalf("optional requestBody must be required=false: %v", body)
	}
}

func TestBuildRejectsDuplicateOperation(t *testing.T) {
	doc := Doc{
		Title: "dup",
		Endpoints: []Endpoint{
			{Method: http.MethodGet, Path: "/v1/a", Summary: "a", Docs: []DocKind{DataPlane}},
			{Method: http.MethodGet, Path: "/v1/a", Summary: "b", Docs: []DocKind{DataPlane}},
		},
	}
	if _, err := Build(doc); err == nil {
		t.Fatalf("expected duplicate operation error")
	}
}

func TestBuildRejectsDuplicateResponseStatus(t *testing.T) {
	doc := Doc{
		Title: "dup responses",
		Endpoints: []Endpoint{
			{
				Method:  http.MethodGet,
				Path:    "/v1/a",
				Summary: "a",
				Docs:    []DocKind{DataPlane},
				Responses: []Response{
					{Code: 200, Description: "first", Schema: &SchemaRef{Type: reflect.TypeOf(testNested{})}},
					{Code: 200, Description: "second", Schema: &SchemaRef{Type: reflect.TypeOf(testPayload{})}},
				},
			},
		},
	}
	if _, err := Build(doc); err == nil {
		t.Fatalf("expected duplicate response status error")
	}
}

func TestBuildRequiredQueryParamRoundTrip(t *testing.T) {
	doc := Doc{
		Title: "required query",
		Endpoints: []Endpoint{
			{
				Method:  http.MethodGet,
				Path:    "/v1/search",
				Summary: "search",
				Docs:    []DocKind{DataPlane},
				Parameters: []Parameter{
					{In: "query", Name: "q", Required: true, Schema: String()},
					{In: "query", Name: "limit", Schema: Integer("int64")},
				},
			},
		},
	}
	raw, err := Build(doc)
	if err != nil {
		t.Fatalf("Build: %v", err)
	}
	var parsed map[string]any
	if err := json.Unmarshal(raw, &parsed); err != nil {
		t.Fatalf("output is not valid JSON: %v", err)
	}
	params := parsed["paths"].(map[string]any)["/v1/search"].(map[string]any)["get"].(map[string]any)["parameters"].([]any)
	byName := map[string]map[string]any{}
	for _, p := range params {
		pm := p.(map[string]any)
		byName[pm["name"].(string)] = pm
	}
	if byName["q"]["required"] != true {
		t.Fatalf("required query param must round-trip required=true: %v", byName["q"])
	}
	if byName["limit"]["required"] != false {
		t.Fatalf("optional query param must round-trip required=false: %v", byName["limit"])
	}
}

func TestBuildBodylessResponses(t *testing.T) {
	doc := Doc{
		Title: "bodyless",
		Endpoints: []Endpoint{
			{
				Method:  http.MethodGet,
				Path:    "/v1/stream",
				Summary: "sse",
				Docs:    []DocKind{DataPlane},
				Responses: []Response{
					{Code: 200, Description: "SSE stream", MediaType: "text/event-stream"},
				},
			},
			{
				Method:  http.MethodPost,
				Path:    "/v1/accept",
				Summary: "accepted",
				Docs:    []DocKind{DataPlane},
				Responses: []Response{
					{Code: 204, Description: "No content"},
				},
			},
			{
				Method:  http.MethodGet,
				Path:    "/v1/redirect",
				Summary: "redirect",
				Docs:    []DocKind{DataPlane},
				Responses: []Response{
					{Code: 302, Description: "Redirect"},
				},
			},
		},
	}
	raw, err := Build(doc)
	if err != nil {
		t.Fatalf("Build: %v", err)
	}
	var parsed map[string]any
	if err := json.Unmarshal(raw, &parsed); err != nil {
		t.Fatalf("output is not valid JSON: %v", err)
	}
	paths := parsed["paths"].(map[string]any)
	stream := paths["/v1/stream"].(map[string]any)["get"].(map[string]any)["responses"].(map[string]any)["200"].(map[string]any)
	if got := stream["content"].(map[string]any); len(got) != 1 || got["text/event-stream"] == nil {
		t.Fatalf("SSE content = %v, want only text/event-stream", got)
	}
	accepted := paths["/v1/accept"].(map[string]any)["post"].(map[string]any)["responses"].(map[string]any)["204"].(map[string]any)
	if _, ok := accepted["content"]; ok {
		t.Fatalf("204 must not declare content: %v", accepted)
	}
	redirect := paths["/v1/redirect"].(map[string]any)["get"].(map[string]any)["responses"].(map[string]any)["302"].(map[string]any)
	if _, ok := redirect["content"]; ok {
		t.Fatalf("302 must not declare content: %v", redirect)
	}
}

func TestBuildRejectsUnresolvedRefInInlineRaw(t *testing.T) {
	// A typo'd $ref inside an unnamed Raw schema inlined into paths must
	// fail Build (checkDocumentRefs walks the assembled document).
	doc := Doc{
		Title: "bad inline ref",
		Endpoints: []Endpoint{
			{
				Method:  http.MethodGet,
				Path:    "/v1/bad",
				Summary: "bad",
				Docs:    []DocKind{DataPlane},
				Responses: []Response{
					{Code: 200, Description: "bad", Schema: &SchemaRef{Raw: map[string]any{
						"type":  "array",
						"items": map[string]any{"$ref": "#/components/schemas/typo.Missing"},
					}}},
				},
			},
		},
	}
	if _, err := Build(doc); err == nil {
		t.Fatalf("expected unresolved inline $ref error")
	}
}

func TestBuildRejectsUnknownScheme(t *testing.T) {
	doc := Doc{
		Title:    "bad scheme",
		Security: []map[string][]string{{"Nope": {}}},
	}
	if _, err := Build(doc); err == nil {
		t.Fatalf("expected unknown scheme error")
	}
}

func TestBuildRejectsMissingPathParam(t *testing.T) {
	doc := Doc{
		Title: "bad param",
		Endpoints: []Endpoint{
			{Method: http.MethodGet, Path: "/v1/a", Summary: "a", Docs: []DocKind{DataPlane},
				Parameters: []Parameter{{In: "path", Name: "missing", Required: true, Schema: String()}}},
		},
	}
	if _, err := Build(doc); err == nil {
		t.Fatalf("expected missing path parameter error")
	}
}
