package validjsonschema

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"sync/atomic"
	"testing"

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

	"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)

func TestValidatorAcceptsSchemaConformantJSON(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithSchema(`{"name":"Aria"}`, objectSchema()))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if !result.Approved {
		t.Fatalf("Validate() = %#v, want approved", result)
	}
}

func TestValidatorRejectsInvalidPayloadJSON(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithSchema(`{"name":`, objectSchema()))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if result.Approved {
		t.Fatalf("Approved = true, want false")
	}
	if result.ReasonCode != ReasonCodeInvalidJSON {
		t.Fatalf("ReasonCode = %q, want %q", result.ReasonCode, ReasonCodeInvalidJSON)
	}
}

func TestValidatorRejectsSchemaNonConformance(t *testing.T) {
	result, err := New(Options{}).Validate(context.Background(), requestWithSchema(`{"name":3}`, objectSchema()))
	if err != nil {
		t.Fatalf("Validate() error = %v, want nil", err)
	}
	if result.Approved {
		t.Fatalf("Approved = true, want false")
	}
	if result.ReasonCode != ReasonCodeSchemaInvalid {
		t.Fatalf("ReasonCode = %q, want %q", result.ReasonCode, ReasonCodeSchemaInvalid)
	}
}

func TestValidatorErrorsWhenSchemaContentMissing(t *testing.T) {
	_, err := New(Options{}).Validate(context.Background(), requestWithSchema(`{"name":"Aria"}`, nil))
	if err == nil {
		t.Fatal("Validate() error = nil, want missing schema content error")
	}
	if !strings.Contains(err.Error(), "schema content") {
		t.Fatalf("Validate() error = %q, want schema content context", err.Error())
	}
}

func TestValidatorErrorsWhenSchemaContentIsMalformed(t *testing.T) {
	_, err := New(Options{}).Validate(context.Background(), requestWithSchema(`{"name":"Aria"}`, []byte(`{"type":`)))
	if err == nil {
		t.Fatal("Validate() error = nil, want malformed schema error")
	}
	if !strings.Contains(err.Error(), "parse response schema") {
		t.Fatalf("Validate() error = %q, want parse schema context", err.Error())
	}
}

func TestValidatorCachesEachSchemaIdentity(t *testing.T) {
	validator := New(Options{})
	var compilations atomic.Int32
	validator.compile = func(content []byte) (*jsonschema.Schema, error) {
		compilations.Add(1)
		return compileSchema(content)
	}

	request := requestWithSchema(`{"name":"Aria"}`, objectSchema())
	requests := []contracts.SerializedValidationRequest{
		request,
		withSchema(request, func(schema *contracts.ArtifactSchema) { schema.JSONSchema = append(schema.JSONSchema, ' ') }),
		withSchema(request, func(schema *contracts.ArtifactSchema) { schema.ID = "test.schema.other" }),
		withSchema(request, func(schema *contracts.ArtifactSchema) { schema.Name = "test_schema_other" }),
		withSchema(request, func(schema *contracts.ArtifactSchema) { schema.Version = "v2" }),
	}
	for index, candidate := range requests {
		for attempt := 0; attempt < 2; attempt++ {
			result, err := validator.Validate(context.Background(), candidate)
			if err != nil || !result.Approved {
				t.Fatalf("Validate(request %d, attempt %d) = %#v, %v", index, attempt, result, err)
			}
		}
	}
	if got, want := compilations.Load(), int32(len(requests)); got != want {
		t.Fatalf("schema compilations = %d, want %d", got, want)
	}
}

func TestValidatorCompilesConcurrentSchemaOnce(t *testing.T) {
	validator := New(Options{})
	var compilations atomic.Int32
	validator.compile = func(content []byte) (*jsonschema.Schema, error) {
		compilations.Add(1)
		return compileSchema(content)
	}
	request := requestWithSchema(`{"name":"Aria"}`, objectSchema())

	const validators = 32
	var group sync.WaitGroup
	errs := make(chan error, validators)
	group.Add(validators)
	for index := 0; index < validators; index++ {
		go func() {
			defer group.Done()
			result, err := validator.Validate(context.Background(), request)
			if err != nil {
				errs <- err
				return
			}
			if !result.Approved {
				errs <- fmt.Errorf("Validate() = %#v, want approved", result)
			}
		}()
	}
	group.Wait()
	close(errs)
	for err := range errs {
		t.Error(err)
	}
	if got := compilations.Load(); got != 1 {
		t.Fatalf("schema compilations = %d, want 1", got)
	}
}

func TestValidatorCachesSchemaCompilationErrors(t *testing.T) {
	validator := New(Options{})
	var compilations atomic.Int32
	validator.compile = func(content []byte) (*jsonschema.Schema, error) {
		compilations.Add(1)
		return compileSchema(content)
	}
	request := requestWithSchema(`{"name":"Aria"}`, []byte(`{"type":`))

	_, first := validator.Validate(context.Background(), request)
	_, second := validator.Validate(context.Background(), request)
	if first == nil || second == nil {
		t.Fatalf("Validate() errors = %v, %v, want cached schema errors", first, second)
	}
	if first.Error() != second.Error() {
		t.Fatalf("cached schema errors differ: %q and %q", first, second)
	}
	if got := compilations.Load(); got != 1 {
		t.Fatalf("schema compilations = %d, want 1", got)
	}
}

func TestSpecAndRegister(t *testing.T) {
	if Spec().Key != Key || Spec().ExecutionClass != contracts.ExecutionClassDeterministic {
		t.Fatalf("Spec() = %#v, want key and deterministic execution", Spec())
	}

	registry := pipeline.NewValidatorRegistry()
	if err := Register(registry); err != nil {
		t.Fatalf("Register() error = %v, want nil", err)
	}
	if registered, ok := registry.Spec(Key); !ok || registered.Key != Key {
		t.Fatalf("Spec(%q) = %#v, %v", Key, registered, ok)
	}
}

func requestWithSchema(payload string, schema []byte) contracts.SerializedValidationRequest {
	return contracts.SerializedValidationRequest{
		Schema: contracts.ArtifactSchema{
			ID:         "test.schema",
			Name:       "test_schema",
			Version:    "v1",
			JSONSchema: append([]byte(nil), schema...),
		},
		Content: []byte(payload), MediaType: "application/json",
	}
}

func withSchema(request contracts.SerializedValidationRequest, mutate func(*contracts.ArtifactSchema)) contracts.SerializedValidationRequest {
	request.Schema = contracts.CloneArtifactSchema(request.Schema)
	mutate(&request.Schema)
	return request
}

func objectSchema() []byte {
	return []byte(`{
		"$schema": "https://json-schema.org/draft/2020-12/schema",
		"type": "object",
		"required": ["name"],
		"properties": {
			"name": {"type": "string"}
		},
		"additionalProperties": false
	}`)
}
