package exec

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

	"github.com/santhosh-tekuri/jsonschema/v6"
)

func TestWorkflowOutputSchemasAreClosedAndAcceptWireEquivalentPages(t *testing.T) {
	const (
		plan = `{"id":"plan-1","scopeId":"scope-1","key":"key","displayName":"plan","descriptionMd":"","state":"active","revision":1,"createdBy":{"type":"agent","id":"agent-1","memberId":"member-1"},"createdAt":"2026-01-01T00:00:00Z","updatedBy":{"type":"agent","id":"agent-1","memberId":"member-1"},"updatedAt":"2026-01-01T00:00:00Z"}`
		run  = `{"id":"run-1","planId":"plan-1","planVersionId":"version-1","status":"queued","queuedAt":"2026-01-01T00:00:00Z"}`
	)
	tests := []struct {
		name       string
		describe   func() (actionDescriptor, error)
		validPages []string
		invalid    []string
		pageClass  bool
	}{
		{
			name: "plan", describe: func() (actionDescriptor, error) { return describe(workflowPlanSpec) },
			validPages: []string{`{"items":[]}`, `{"items":[` + plan + `]}`},
			invalid: []string{
				`{"items":[],"unknown":true}`,
				`{"items":[{"id":"plan-1","unknown":true}]}`,
			},
			pageClass: true,
		},
		{
			name: "run", describe: func() (actionDescriptor, error) { return describe(workflowRunSpec) },
			validPages: []string{`{"items":[]}`, `{"items":[` + run + `]}`},
			invalid: []string{
				`{"items":[],"unknown":true}`,
				`{"items":[{"id":"run-1","unknown":true}]}`,
			},
			pageClass: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			descriptor, err := tt.describe()
			if err != nil {
				t.Fatal(err)
			}
			compiled := mustCompile(t, descriptor.OutputSchema)
			for _, data := range tt.validPages {
				raw := jsonRaw(`{"data":` + data + `}`)
				if err := validateInstance(compiled, raw); err != nil {
					t.Errorf("valid page rejected: %s: %v", data, err)
				}
			}
			for _, data := range tt.invalid {
				raw := jsonRaw(`{"data":` + data + `}`)
				if err := validateInstance(compiled, raw); err == nil {
					t.Errorf("invalid page accepted: %s", data)
				}
			}
			var schema map[string]any
			if err := json.Unmarshal(descriptor.OutputSchema, &schema); err != nil {
				t.Fatal(err)
			}
			data := schema["properties"].(map[string]any)["data"].(map[string]any)
			topBranches := data["oneOf"].([]any)
			for _, page := range tt.validPages {
				if got := matchingSchemaBranches(t, topBranches, jsonRaw(page)); got != 1 {
					t.Errorf("page must match exactly one top-level branch, got %d: %s", got, page)
				}
			}
			if tt.pageClass {
				pageBranch := findNestedOneOfBranch(t, topBranches)
				internalBranches := pageBranch["oneOf"].([]any)
				for _, page := range tt.validPages {
					if got := matchingSchemaBranches(t, internalBranches, jsonRaw(page)); got != 1 {
						t.Errorf("page must match exactly one internal page-class branch, got %d: %s", got, page)
					}
				}
			}
			if data["additionalProperties"] == true {
				t.Fatalf("root data schema remains open: %s", descriptor.OutputSchema)
			}
			if strings.Contains(string(descriptor.OutputSchema), `"discriminator"`) {
				t.Fatalf("wire discriminator introduced: %s", descriptor.OutputSchema)
			}
		})
	}
}

func matchingSchemaBranches(t *testing.T, branches []any, instance json.RawMessage) int {
	t.Helper()
	matches := 0
	for _, branch := range branches {
		raw, err := json.Marshal(branch)
		if err != nil {
			t.Fatal(err)
		}
		compiled := mustCompile(t, raw)
		if validateInstance(compiled, instance) == nil {
			matches++
		}
	}
	return matches
}

func findNestedOneOfBranch(t *testing.T, branches []any) map[string]any {
	t.Helper()
	var found map[string]any
	for _, branch := range branches {
		candidate, ok := branch.(map[string]any)
		if !ok || candidate["oneOf"] == nil {
			continue
		}
		if found != nil {
			t.Fatal("multiple nested page-class branches")
		}
		found = candidate
	}
	if found == nil {
		t.Fatal("nested page-class branch not found")
	}
	return found
}

func mustCompile(t *testing.T, schema []byte) *jsonschema.Schema {
	t.Helper()
	s, err := compileSchema(schema)
	if err != nil {
		t.Fatalf("compileSchema: %v", err)
	}
	return s
}

func TestCompileSchemaRejectsMalformed(t *testing.T) {
	if _, err := compileSchema(jsonRaw(`{not json`)); err == nil {
		t.Fatal("expected error for invalid JSON schema")
	}
}

// TestRealSchemaValidator covers the JSON Schema constructs used by action contracts.
func TestRealSchemaValidator(t *testing.T) {
	t.Run("additionalProperties false rejects unknown", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"type":"object","additionalProperties":false,"properties":{"a":{"type":"string"}}}`))
		if err := validateInstance(sch, jsonRaw(`{"a":"x","unknown":1}`)); err == nil {
			t.Fatal("expected unknown field rejection")
		}
		if err := validateInstance(sch, jsonRaw(`{"a":"x"}`)); err != nil {
			t.Fatalf("valid instance rejected: %v", err)
		}
	})
	t.Run("nested required", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"type":"object","properties":{"o":{"type":"object","required":["x"]}}}`))
		if err := validateInstance(sch, jsonRaw(`{"o":{}}`)); err == nil {
			t.Fatal("expected nested required rejection")
		}
		if err := validateInstance(sch, jsonRaw(`{"o":{"x":1}}`)); err != nil {
			t.Fatalf("valid nested rejected: %v", err)
		}
	})
	t.Run("oneOf accepts branch rejects none", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"oneOf":[{"required":["a"]},{"required":["b"]}]}`))
		if err := validateInstance(sch, jsonRaw(`{}`)); err == nil {
			t.Fatal("expected oneOf rejection for {}")
		}
		if err := validateInstance(sch, jsonRaw(`{"a":1}`)); err != nil {
			t.Fatalf("valid oneOf branch rejected: %v", err)
		}
	})
	t.Run("enum", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"enum":["red","green"]}`))
		if err := validateInstance(sch, jsonRaw(`"blue"`)); err == nil {
			t.Fatal("expected enum rejection")
		}
		if err := validateInstance(sch, jsonRaw(`"red"`)); err != nil {
			t.Fatalf("valid enum rejected: %v", err)
		}
	})
	t.Run("array items", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"type":"array","items":{"type":"integer"}}`))
		if err := validateInstance(sch, jsonRaw(`[1,2,3]`)); err != nil {
			t.Fatalf("valid array rejected: %v", err)
		}
		if err := validateInstance(sch, jsonRaw(`[1,"x"]`)); err == nil {
			t.Fatal("expected array item type rejection")
		}
	})
}

// TestDraftPolicy permits only the package Draft 2020-12 policy and keeps format annotation-only.
func TestDraftPolicy(t *testing.T) {
	t.Run("no $schema compiles", func(t *testing.T) {
		if _, err := compileSchema(jsonRaw(`{"type":"object"}`)); err != nil {
			t.Fatal(err)
		}
	})
	t.Run("explicit Draft2020 $schema compiles", func(t *testing.T) {
		if _, err := compileSchema(jsonRaw(`{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object"}`)); err != nil {
			t.Fatal(err)
		}
	})
	t.Run("draft-07 $schema rejected", func(t *testing.T) {
		if _, err := compileSchema(jsonRaw(`{"$schema":"http://json-schema.org/draft-07/schema#","type":"object"}`)); err == nil {
			t.Fatal("expected draft-07 rejection")
		}
	})
	t.Run("annotation-only format accepts invalid email", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"type":"string","format":"email"}`))
		if err := validateInstance(sch, jsonRaw(`"not-an-email"`)); err != nil {
			t.Fatalf("format should be annotation-only: %v", err)
		}
	})
}

// TestIntegerPrecision keeps integers beyond 2^53 exact.
func TestIntegerPrecision(t *testing.T) {
	sch := mustCompile(t, jsonRaw(`{"type":"integer","const":9007199254740993}`)) // 2^53+1
	if err := validateInstance(sch, jsonRaw(`9007199254740993`)); err != nil {
		t.Fatalf("expected const match: %v", err)
	}
	if err := validateInstance(sch, jsonRaw(`9007199254740992`)); err == nil {
		t.Fatal("expected adjacent integer (2^53) to be rejected")
	}
}

// TestIntegerPrecisionBoundary covers numeric constraints beyond 2^53.
func TestIntegerPrecisionBoundary(t *testing.T) {
	t.Run("enum", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"enum":[9007199254740993,9007199254740995]}`))
		if err := validateInstance(sch, jsonRaw(`9007199254740993`)); err != nil {
			t.Fatalf("expected enum match: %v", err)
		}
		if err := validateInstance(sch, jsonRaw(`9007199254740994`)); err == nil {
			t.Fatal("expected non-enum adjacent integer rejected")
		}
	})
	t.Run("minimum", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"type":"integer","minimum":9007199254740993}`))
		if err := validateInstance(sch, jsonRaw(`9007199254740993`)); err != nil {
			t.Fatalf("expected minimum boundary: %v", err)
		}
		if err := validateInstance(sch, jsonRaw(`9007199254740992`)); err == nil {
			t.Fatal("expected below-minimum adjacent integer rejected")
		}
	})
	t.Run("maximum", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"type":"integer","maximum":9007199254740993}`))
		if err := validateInstance(sch, jsonRaw(`9007199254740993`)); err != nil {
			t.Fatalf("expected maximum boundary: %v", err)
		}
		if err := validateInstance(sch, jsonRaw(`9007199254740994`)); err == nil {
			t.Fatal("expected above-maximum adjacent integer rejected")
		}
	})
	t.Run("multipleOf", func(t *testing.T) {
		sch := mustCompile(t, jsonRaw(`{"type":"integer","multipleOf":9007199254740993}`))
		if err := validateInstance(sch, jsonRaw(`9007199254740993`)); err != nil {
			t.Fatalf("expected multiple match: %v", err)
		}
		if err := validateInstance(sch, jsonRaw(`9007199254740994`)); err == nil {
			t.Fatal("expected non-multiple adjacent integer rejected")
		}
	})
}

// TestDecodeStrictTrailing rejects a second JSON value.
func TestDecodeStrictTrailing(t *testing.T) {
	for _, raw := range []string{`{} {}`, `1 2`, `"a" "b"`, `{}{}`} {
		if _, err := decodeStrict([]byte(raw)); err == nil {
			t.Fatalf("decodeStrict(%q) expected trailing-JSON error", raw)
		}
	}
	if _, err := decodeStrict([]byte(`{"a":1}`)); err != nil {
		t.Fatalf("single value rejected: %v", err)
	}
}

// TestDraftPolicyRecursive applies the dialect restriction to nested schema resources.
func TestDraftPolicyRecursive(t *testing.T) {
	t.Run("nested $defs draft-07 rejected", func(t *testing.T) {
		schema := jsonRaw(`{"type":"object","$defs":{"x":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object"}}}`)
		if _, err := compileSchema(schema); err == nil {
			t.Fatal("expected nested draft-07 $schema rejection")
		}
	})
	t.Run("unknown $schema rejected", func(t *testing.T) {
		if _, err := compileSchema(jsonRaw(`{"$schema":"https://example.com/whatever","type":"object"}`)); err == nil {
			t.Fatal("expected unknown dialect rejection")
		}
	})
	t.Run("non-string $schema rejected", func(t *testing.T) {
		if _, err := compileSchema(jsonRaw(`{"$schema":123,"type":"object"}`)); err == nil {
			t.Fatal("expected non-string $schema rejection")
		}
	})
	t.Run("nested Draft2020 $schema ok", func(t *testing.T) {
		schema := jsonRaw(`{"type":"object","$defs":{"x":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object"}}}`)
		if _, err := compileSchema(schema); err != nil {
			t.Fatalf("nested Draft2020 $schema should compile: %v", err)
		}
	})
}

// TestDraftPolicyDataNotRejected treats $schema inside data-valued keywords as data.
func TestDraftPolicyDataNotRejected(t *testing.T) {
	for _, raw := range []string{
		`{"type":"object","const":{"$schema":"ordinary data"}}`,
		`{"type":"object","enum":[{"$schema":"ordinary data"}]}`,
		`{"type":"object","examples":[{"$schema":"ordinary data"}]}`,
		`{"type":"object","default":{"$schema":"ordinary data"}}`,
	} {
		if _, err := compileSchema([]byte(raw)); err != nil {
			t.Errorf("compileSchema(%s) rejected data $schema: %v", raw, err)
		}
	}
}

// TestDraftPolicyContentSchema applies dialect checks below contentSchema.
func TestDraftPolicyContentSchema(t *testing.T) {
	raw := `{"type":"string","contentSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"string"}}`
	if _, err := compileSchema([]byte(raw)); err == nil {
		t.Fatal("expected nested draft-07 $schema under contentSchema to be rejected")
	}
}

// TestDraftPolicyObsoleteKeywordsRejected blocks keywords from older schema dialects
// that could introduce a nested dialect resource.
func TestDraftPolicyObsoleteKeywordsRejected(t *testing.T) {
	for _, kw := range []string{"definitions", "dependencies", "additionalItems"} {
		raw := `{"type":"object","` + kw + `":{"x":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object"}}}`
		if _, err := compileSchema([]byte(raw)); err == nil {
			t.Errorf("expected obsolete keyword %q to be rejected", kw)
		}
	}
}
