package scenes

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

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

func TestLoadResponseSchemaForScenes(t *testing.T) {
	schema, err := loadResponseSchema()
	if err != nil {
		t.Fatalf("loadResponseSchema() error = %v, want nil", err)
	}
	if got, want := string(schema.Key), "dnd_scenes_llm"; got != want {
		t.Fatalf("schema.Key = %q, want %q", got, want)
	}
	if got, want := schema.ID, "notarius.dnd.scenes.llm"; got != want {
		t.Fatalf("schema.ID = %q, want %q", got, want)
	}
	if got, want := schema.Version, "v1"; got != want {
		t.Fatalf("schema.Version = %q, want %q", got, want)
	}
	if got, want := schema.Name, "notarius_dnd_scenes_llm_v1"; got != want {
		t.Fatalf("schema.Name = %q, want %q", got, want)
	}
	if !strings.HasPrefix(schema.SHA256, "sha256:") {
		t.Fatalf("schema.SHA256 = %q, want sha256 prefix", schema.SHA256)
	}
	if !json.Valid(schema.JSONSchema) {
		t.Fatalf("schema.JSONSchema is invalid JSON: %s", schema.JSONSchema)
	}
}

func TestResponseSchemaValidatesMinimalSceneResponses(t *testing.T) {
	schema, err := loadResponseSchema()
	if err != nil {
		t.Fatalf("loadResponseSchema() error = %v, want nil", err)
	}

	valid := validSceneSchemaResponse()
	validJSON, err := json.Marshal(valid)
	if err != nil {
		t.Fatalf("Marshal(validSceneSchemaResponse()) error = %v, want nil", err)
	}
	if err := validateJSONSchema(validJSON, schema.JSONSchema); err != nil {
		t.Fatalf("valid scene response rejected: %v", err)
	}

	tests := []struct {
		name   string
		mutate func(map[string]any)
	}{
		{
			name: "missing scenes",
			mutate: func(response map[string]any) {
				delete(response, "scenes")
			},
		},
		{
			name: "empty scenes",
			mutate: func(response map[string]any) {
				response["scenes"] = []any{}
			},
		},
		{
			name: "non-positive start unit",
			mutate: func(response map[string]any) {
				response["scenes"].([]any)[0].(map[string]any)["start_unit_id"] = 0
			},
		},
		{
			name: "non-positive end unit",
			mutate: func(response map[string]any) {
				response["scenes"].([]any)[0].(map[string]any)["end_unit_id"] = 0
			},
		},
		{
			name: "non-integer endpoint",
			mutate: func(response map[string]any) {
				response["scenes"].([]any)[0].(map[string]any)["start_unit_id"] = 1.5
			},
		},
		{
			name: "unknown top-level field",
			mutate: func(response map[string]any) {
				response["boundary_caveats"] = []any{}
			},
		},
		{
			name: "unknown scene field",
			mutate: func(response map[string]any) {
				response["scenes"].([]any)[0].(map[string]any)["short_title"] = "Old contract"
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			response := validSceneSchemaResponse()
			tt.mutate(response)
			content, err := json.Marshal(response)
			if err != nil {
				t.Fatalf("Marshal() error = %v, want nil", err)
			}
			if err := validateJSONSchema(content, schema.JSONSchema); err == nil {
				t.Fatal("validateJSONSchema() error = nil, want rejected response")
			}
		})
	}
}

func TestResponseStructAcceptsIntegerBoundaries(t *testing.T) {
	raw := []byte(`{"scenes":[{"start_unit_id":1,"end_unit_id":3}]}`)

	var response chunkResponse
	if err := json.Unmarshal(raw, &response); err != nil {
		t.Fatalf("Unmarshal() error = %v, want nil", err)
	}
	if got := response.Scenes[0].StartUnitID.String(); got != "1" {
		t.Fatalf("StartUnitID = %q, want 1", got)
	}
	if got := response.Scenes[0].EndUnitID.String(); got != "3" {
		t.Fatalf("EndUnitID = %q, want 3", got)
	}
}

func TestResponseSchemaJSONIsMutationSafe(t *testing.T) {
	first, err := loadResponseSchema()
	if err != nil {
		t.Fatalf("loadResponseSchema() error = %v, want nil", err)
	}
	first.JSONSchema[0] = '['

	second, err := loadResponseSchema()
	if err != nil {
		t.Fatalf("loadResponseSchema() error = %v, want nil", err)
	}
	if !json.Valid(second.JSONSchema) {
		t.Fatalf("schema JSON was mutated: %s", second.JSONSchema)
	}
	if len(second.JSONSchema) > 0 && second.JSONSchema[0] == '[' {
		t.Fatal("schema JSON did not use defensive copy")
	}
}

func validSceneSchemaResponse() map[string]any {
	return map[string]any{
		"scenes": []any{
			map[string]any{
				"start_unit_id": 1,
				"end_unit_id":   3,
			},
		},
	}
}

func validateJSONSchema(instanceContent, schemaContent []byte) error {
	instance, err := jsonschema.UnmarshalJSON(bytes.NewReader(instanceContent))
	if err != nil {
		return fmt.Errorf("parse instance: %w", err)
	}
	schemaDocument, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaContent))
	if err != nil {
		return fmt.Errorf("parse schema: %w", err)
	}
	compiler := jsonschema.NewCompiler()
	if err := compiler.AddResource("schema.json", schemaDocument); err != nil {
		return fmt.Errorf("load schema: %w", err)
	}
	schema, err := compiler.Compile("schema.json")
	if err != nil {
		return fmt.Errorf("compile schema: %w", err)
	}
	return schema.Validate(instance)
}
