package pipeline

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"reflect"
	"strings"
	"testing"

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

type codecNotes struct {
	Items   []string          `json:"items"`
	Labels  map[string]string `json:"labels,omitempty"`
	Details *codecNoteDetails `json:"details,omitempty"`
}

type codecNoteDetails struct {
	Name string `json:"name"`
}

type codecScore struct {
	Value int `json:"value"`
}

type codecNotesAlias codecNotes

type testArtifactCodec[T any] struct {
	kind                contracts.ArtifactKind
	schema              contracts.ArtifactSchema
	mediaType           string
	encodeFunc          func(T) ([]byte, error)
	candidateFunc       func(T) ([]byte, error)
	decodeFunc          func([]byte) (T, error)
	candidateDecodeFunc func([]byte) (T, error)
}

func (c testArtifactCodec[T]) Kind() contracts.ArtifactKind     { return c.kind }
func (c testArtifactCodec[T]) Schema() contracts.ArtifactSchema { return c.schema }
func (c testArtifactCodec[T]) MediaType() string                { return c.mediaType }
func (c testArtifactCodec[T]) EncodeCandidate(value T) ([]byte, error) {
	if c.candidateFunc != nil {
		return c.candidateFunc(value)
	}
	return c.encodeFunc(value)
}
func (c testArtifactCodec[T]) Encode(value T) ([]byte, error)   { return c.encodeFunc(value) }
func (c testArtifactCodec[T]) Decode(content []byte) (T, error) { return c.decodeFunc(content) }
func (c testArtifactCodec[T]) DecodeCandidate(content []byte) (T, error) {
	if c.candidateDecodeFunc != nil {
		return c.candidateDecodeFunc(content)
	}
	return c.decodeFunc(content)
}

var _ contracts.ArtifactCodec[codecNotes] = testArtifactCodec[codecNotes]{}
var _ contracts.CandidateArtifactCodec[codecNotes] = testArtifactCodec[codecNotes]{}

func TestArtifactCodecRegistryStoresHeterogeneousExactTypes(t *testing.T) {
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, notesCodec()); err != nil {
		t.Fatalf("RegisterArtifactCodec(notes) error = %v, want nil", err)
	}
	if err := RegisterArtifactCodec(registry, scoreCodec()); err != nil {
		t.Fatalf("RegisterArtifactCodec(score) error = %v, want nil", err)
	}

	if got, want := registry.RegisteredKinds(), []contracts.ArtifactKind{"test/notes", "test/score"}; !reflect.DeepEqual(got, want) {
		t.Fatalf("RegisteredKinds() = %#v, want %#v", got, want)
	}
	notes := codecNotes{Items: []string{"second", "first"}}
	first, err := registry.Encode("test/notes", notes)
	if err != nil {
		t.Fatalf("Encode(notes) error = %v, want nil", err)
	}
	second, err := registry.Encode(" test/notes ", notes)
	if err != nil {
		t.Fatalf("Encode(notes again) error = %v, want nil", err)
	}
	if !bytes.Equal(first.Content, second.Content) {
		t.Fatalf("equal values encoded as %q and %q, want deterministic bytes", first.Content, second.Content)
	}
	decodedNotes, err := registry.Decode(first)
	if err != nil {
		t.Fatalf("Decode(notes) error = %v, want nil", err)
	}
	if !reflect.DeepEqual(decodedNotes, notes) {
		t.Fatalf("Decode(notes) = %#v, want %#v", decodedNotes, notes)
	}

	score := codecScore{Value: 17}
	serializedScore, err := registry.Encode("test/score", score)
	if err != nil {
		t.Fatalf("Encode(score) error = %v, want nil", err)
	}
	decodedScore, err := registry.Decode(serializedScore)
	if err != nil {
		t.Fatalf("Decode(score) error = %v, want nil", err)
	}
	if decodedScore != score {
		t.Fatalf("Decode(score) = %#v, want %#v", decodedScore, score)
	}

	_, err = registry.Encode("test/notes", codecNotesAlias(notes))
	var typeErr *ArtifactCodecTypeError
	if !errors.As(err, &typeErr) {
		t.Fatalf("Encode(alias) error = %T %v, want ArtifactCodecTypeError", err, err)
	}
	if typeErr.ExpectedType == typeErr.ActualType {
		t.Fatalf("type error = %#v, want distinct exact types", typeErr)
	}
}

func TestArtifactCodecRegistryKeepsCandidateAndFinalEncodingDistinct(t *testing.T) {
	candidateCalls, finalCalls := 0, 0
	codec := notesCodec()
	codec.candidateFunc = func(codecNotes) ([]byte, error) {
		candidateCalls++
		return []byte(`{"items":["candidate"]}`), nil
	}
	codec.encodeFunc = func(codecNotes) ([]byte, error) {
		finalCalls++
		return []byte(`{"items":["final"]}`), nil
	}
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, codec); err != nil {
		t.Fatalf("RegisterArtifactCodec() error = %v, want nil", err)
	}
	entry, _, err := registry.entry(codec.kind)
	if err != nil {
		t.Fatalf("entry() error = %v, want nil", err)
	}

	candidate, err := serializeArtifact(entry, codecNotes{}, true)
	if err != nil {
		t.Fatalf("serialize candidate error = %v, want nil", err)
	}
	if string(candidate.Content) != `{"items":["candidate"]}` || candidateCalls != 1 || finalCalls != 0 {
		t.Fatalf("candidate content = %s, calls = candidate %d, final %d", candidate.Content, candidateCalls, finalCalls)
	}

	final, err := serializeArtifact(entry, codecNotes{}, false)
	if err != nil {
		t.Fatalf("serialize final error = %v, want nil", err)
	}
	if string(final.Content) != `{"items":["final"]}` || candidateCalls != 1 || finalCalls != 1 {
		t.Fatalf("final content = %s, calls = candidate %d, final %d", final.Content, candidateCalls, finalCalls)
	}
}

func TestArtifactCodecRegistryKeepsCandidateAndFinalDecodingDistinct(t *testing.T) {
	candidateCalls, finalCalls := 0, 0
	codec := notesCodec()
	codec.candidateDecodeFunc = func([]byte) (codecNotes, error) {
		candidateCalls++
		return codecNotes{Items: []string{"candidate"}}, nil
	}
	codec.decodeFunc = func([]byte) (codecNotes, error) {
		finalCalls++
		return codecNotes{Items: []string{"final"}}, nil
	}
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, codec); err != nil {
		t.Fatalf("RegisterArtifactCodec() error = %v, want nil", err)
	}
	entry, _, err := registry.entry(codec.kind)
	if err != nil {
		t.Fatalf("entry() error = %v, want nil", err)
	}

	candidate, err := entry.decodeCandidate([]byte(`{"items":["one"]}`))
	if err != nil {
		t.Fatalf("candidate decode error = %v", err)
	}
	candidateWant := codecNotes{Items: []string{"candidate"}}
	if !reflect.DeepEqual(candidate, candidateWant) || candidateCalls != 1 || finalCalls != 0 {
		t.Fatalf("candidate decode = %#v, calls = candidate %d, final %d", candidate, candidateCalls, finalCalls)
	}
	decoded, err := entry.decode([]byte(`{"items":["one"]}`))
	if err != nil {
		t.Fatalf("final decode error = %v", err)
	}
	finalWant := codecNotes{Items: []string{"final"}}
	if !reflect.DeepEqual(decoded, finalWant) || candidateCalls != 1 || finalCalls != 1 {
		t.Fatalf("final decode = %#v, calls = candidate %d, final %d", decoded, candidateCalls, finalCalls)
	}
}

func TestArtifactCodecRegistryStoresValidatedSchemaMetadata(t *testing.T) {
	registry := NewArtifactCodecRegistry()
	codec := notesCodec()
	codec.kind = " test/notes "
	codec.schema.ID = " notes.v1 "
	codec.schema.Name = " notes "
	codec.schema.Version = " v1 "
	codec.mediaType = " application/json "
	if err := RegisterArtifactCodec(registry, codec); err != nil {
		t.Fatalf("RegisterArtifactCodec() error = %v, want nil", err)
	}
	codec.schema.JSONSchema[0] = '['

	spec, ok := registry.Spec("test/notes")
	if !ok {
		t.Fatal("Spec() ok = false, want true")
	}
	if spec.Kind != "test/notes" || spec.Schema.ID != "notes.v1" || spec.Schema.Name != "notes" || spec.Schema.Version != "v1" || spec.MediaType != "application/json" {
		t.Fatalf("Spec() = %#v, want normalized metadata", spec)
	}
	if spec.SchemaDigest != contracts.DigestArtifactSchema(spec.Schema) {
		t.Fatalf("schema digest = %q, want %q", spec.SchemaDigest, contracts.DigestArtifactSchema(spec.Schema))
	}

	spec.Schema.JSONSchema[0] = '['
	again, _ := registry.Spec("test/notes")
	if string(again.Schema.JSONSchema) != `{"additionalProperties":false,"properties":{"details":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"},"items":{"items":{"type":"string"},"type":"array"},"labels":{"additionalProperties":{"type":"string"},"type":"object"}},"required":["items"],"type":"object"}` {
		t.Fatalf("stored JSON Schema changed through Spec result: %q", again.Schema.JSONSchema)
	}
}

func TestArtifactCodecRegistryRejectsInvalidRegistration(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*testArtifactCodec[codecNotes])
		want   string
	}{
		{name: "kind", mutate: func(codec *testArtifactCodec[codecNotes]) { codec.kind = " " }, want: "kind"},
		{name: "schema id", mutate: func(codec *testArtifactCodec[codecNotes]) { codec.schema.ID = "" }, want: "schema id"},
		{name: "schema name", mutate: func(codec *testArtifactCodec[codecNotes]) { codec.schema.Name = "" }, want: "schema name"},
		{name: "schema version", mutate: func(codec *testArtifactCodec[codecNotes]) { codec.schema.Version = "" }, want: "schema version"},
		{name: "media type", mutate: func(codec *testArtifactCodec[codecNotes]) { codec.mediaType = "" }, want: "media type"},
		{name: "empty JSON Schema", mutate: func(codec *testArtifactCodec[codecNotes]) { codec.schema.JSONSchema = nil }, want: "JSON Schema"},
		{name: "invalid JSON Schema", mutate: func(codec *testArtifactCodec[codecNotes]) { codec.schema.JSONSchema = []byte(`{"type":`) }, want: "valid JSON"},
		{name: "non-schema JSON", mutate: func(codec *testArtifactCodec[codecNotes]) { codec.schema.JSONSchema = []byte(`[]`) }, want: "JSON Schema is invalid"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			registry := NewArtifactCodecRegistry()
			codec := notesCodec()
			test.mutate(&codec)
			if err := RegisterArtifactCodec(registry, codec); err == nil || !strings.Contains(err.Error(), test.want) {
				t.Fatalf("RegisterArtifactCodec() error = %v, want %q", err, test.want)
			}
		})
	}
}

func TestArtifactCodecRegistryRejectsDuplicateKind(t *testing.T) {
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, notesCodec()); err != nil {
		t.Fatalf("RegisterArtifactCodec() error = %v, want nil", err)
	}
	duplicate := notesCodec()
	duplicate.kind = " test/notes "
	if err := RegisterArtifactCodec(registry, duplicate); err == nil || !strings.Contains(err.Error(), "already registered") {
		t.Fatalf("duplicate registration error = %v, want duplicate kind error", err)
	}
}

func TestArtifactCodecRegistryRejectsNilRegistryAndCodec(t *testing.T) {
	codec := notesCodec()
	if err := RegisterArtifactCodec[codecNotes](nil, codec); err == nil || !strings.Contains(err.Error(), "registry") {
		t.Fatalf("nil registry error = %v, want registry error", err)
	}
	var nilCodec *testArtifactCodec[codecNotes]
	if err := RegisterArtifactCodec(NewArtifactCodecRegistry(), nilCodec); err == nil || !strings.Contains(err.Error(), "must not be nil") {
		t.Fatalf("nil codec error = %v, want codec error", err)
	}
}

func TestArtifactCodecRegistryStrictDecodeAndTypedFailures(t *testing.T) {
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, notesCodec()); err != nil {
		t.Fatalf("RegisterArtifactCodec() error = %v, want nil", err)
	}
	valid, err := registry.Encode("test/notes", codecNotes{Items: []string{"one"}})
	if err != nil {
		t.Fatalf("Encode() error = %v, want nil", err)
	}

	for _, content := range [][]byte{
		[]byte(`{"items":["one"],"unknown":true}`),
		[]byte(`{"items":["one"]} {}`),
	} {
		candidate := contracts.CloneSerializedArtifact(valid)
		candidate.Content = content
		_, err := registry.Decode(candidate)
		var operationErr *ArtifactCodecOperationError
		if !errors.As(err, &operationErr) || operationErr.Operation != "decode" {
			t.Fatalf("Decode(%q) error = %T %v, want typed decode error", content, err, err)
		}
	}

	wrongSchema := contracts.CloneSerializedArtifact(valid)
	wrongSchema.Schema.Version = "v2"
	_, err = registry.Decode(wrongSchema)
	var compatibilityErr *ArtifactCodecCompatibilityError
	if !errors.As(err, &compatibilityErr) {
		t.Fatalf("Decode(wrong schema) error = %T %v, want ArtifactCodecCompatibilityError", err, err)
	}
}

func TestArtifactCodecRegistryWrapsEncodeFailure(t *testing.T) {
	codec := notesCodec()
	cause := errors.New("cannot encode notes")
	codec.encodeFunc = func(codecNotes) ([]byte, error) { return nil, cause }
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, codec); err != nil {
		t.Fatalf("RegisterArtifactCodec() error = %v, want nil", err)
	}

	_, err := registry.Encode("test/notes", codecNotes{})
	var operationErr *ArtifactCodecOperationError
	if !errors.As(err, &operationErr) || operationErr.Operation != "encode" || !errors.Is(err, cause) {
		t.Fatalf("Encode() error = %T %v, want typed wrapping encode error", err, err)
	}
}

func TestArtifactCodecRegistryWrapsCandidateDecodeFailure(t *testing.T) {
	cause := errors.New("cannot decode candidate notes")
	codec := notesCodec()
	codec.candidateDecodeFunc = func([]byte) (codecNotes, error) {
		return codecNotes{}, cause
	}
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, codec); err != nil {
		t.Fatalf("RegisterArtifactCodec() error = %v, want nil", err)
	}
	entry, _, err := registry.entry(codec.kind)
	if err != nil {
		t.Fatalf("entry() error = %v, want nil", err)
	}

	_, err = entry.decodeCandidate([]byte(`{"items":["one"]}`))
	var operationErr *ArtifactCodecOperationError
	if !errors.As(err, &operationErr) || operationErr.Operation != "decode candidate" || !errors.Is(err, cause) {
		t.Fatalf("candidate decode error = %T %v, want typed wrapping error", err, err)
	}
}

func TestArtifactCodecRegistryClonesCodecBytes(t *testing.T) {
	shared := []byte(`{"items":["one"]}`)
	codec := notesCodec()
	codec.encodeFunc = func(codecNotes) ([]byte, error) { return shared, nil }
	codec.decodeFunc = func(content []byte) (codecNotes, error) {
		content[0] = '['
		return codecNotes{Items: []string{"one"}}, nil
	}
	codec.candidateDecodeFunc = func(content []byte) (codecNotes, error) {
		content[0] = '['
		return codecNotes{Items: []string{"candidate"}}, nil
	}
	registry := NewArtifactCodecRegistry()
	if err := RegisterArtifactCodec(registry, codec); err != nil {
		t.Fatalf("RegisterArtifactCodec() error = %v, want nil", err)
	}

	artifact, err := registry.Encode("test/notes", codecNotes{})
	if err != nil {
		t.Fatalf("Encode() error = %v, want nil", err)
	}
	shared[0] = '['
	if string(artifact.Content) != `{"items":["one"]}` {
		t.Fatalf("encoded content = %q after codec buffer mutation, want owned bytes", artifact.Content)
	}
	before := append([]byte(nil), artifact.Content...)
	if _, err := registry.Decode(artifact); err != nil {
		t.Fatalf("Decode() error = %v, want nil", err)
	}
	if !bytes.Equal(artifact.Content, before) {
		t.Fatalf("serialized content changed during decode: %q", artifact.Content)
	}
	candidateContent := []byte(`{"items":["candidate"]}`)
	candidateBefore := append([]byte(nil), candidateContent...)
	entry, _, err := registry.entry("test/notes")
	if err != nil {
		t.Fatalf("entry() error = %v, want nil", err)
	}
	if _, err := entry.decodeCandidate(candidateContent); err != nil {
		t.Fatalf("candidate decode error = %v, want nil", err)
	}
	if !bytes.Equal(candidateContent, candidateBefore) {
		t.Fatalf("candidate content changed during decode: %q", candidateContent)
	}
}

func notesCodec() testArtifactCodec[codecNotes] {
	return testArtifactCodec[codecNotes]{
		kind: "test/notes",
		schema: contracts.ArtifactSchema{
			ID:         "notes.v1",
			Name:       "notes",
			Version:    "v1",
			JSONSchema: []byte(`{"additionalProperties":false,"properties":{"details":{"additionalProperties":false,"properties":{"name":{"type":"string"}},"required":["name"],"type":"object"},"items":{"items":{"type":"string"},"type":"array"},"labels":{"additionalProperties":{"type":"string"},"type":"object"}},"required":["items"],"type":"object"}`),
		},
		mediaType: "application/json",
		encodeFunc: func(value codecNotes) ([]byte, error) {
			return json.Marshal(value)
		},
		decodeFunc: func(content []byte) (codecNotes, error) {
			var value codecNotes
			return value, decodeStrictJSON(content, &value)
		},
	}
}

func scoreCodec() testArtifactCodec[codecScore] {
	return testArtifactCodec[codecScore]{
		kind: "test/score",
		schema: contracts.ArtifactSchema{
			ID:         "score.v1",
			Name:       "score",
			Version:    "v1",
			JSONSchema: []byte(`{"additionalProperties":false,"properties":{"value":{"type":"integer"}},"required":["value"],"type":"object"}`),
		},
		mediaType: "application/json",
		encodeFunc: func(value codecScore) ([]byte, error) {
			return json.Marshal(value)
		},
		decodeFunc: func(content []byte) (codecScore, error) {
			var value codecScore
			return value, decodeStrictJSON(content, &value)
		},
	}
}

func decodeStrictJSON(content []byte, out any) error {
	decoder := json.NewDecoder(bytes.NewReader(content))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(out); err != nil {
		return err
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		if err == nil {
			return fmt.Errorf("unexpected trailing JSON value")
		}
		return fmt.Errorf("decode trailing JSON: %w", err)
	}
	return nil
}
