package validator

import (
	"encoding/json"
	"testing"
)

// TestValidationError_MarshalsRustSerdeShape pins the on-wire JSON
// envelope for cross-runtime parity with crates/validator's
// `enum ValidationError`. Surfaced by the 2026-05-02 validator parity
// probe (5/6 body-shape divergence pre-fix → 6/6 match post-fix).
//
// Failing this test = wire-format drift. Run the parity probe at
// scripts/cutover/parity/validator_parity.sh to confirm Rust is
// the source-of-truth shape this test is anchored to.
func TestValidationError_MarshalsRustSerdeShape(t *testing.T) {
	cases := []struct {
		name    string
		err     ValidationError
		want    string
	}{
		{
			name: "schema includes field+reason inside variant body",
			err:  ValidationError{Kind: ErrSchema, Field: "fingerprint", Reason: "missing"},
			want: `{"Schema":{"field":"fingerprint","reason":"missing"}}`,
		},
		{
			name: "completeness only carries reason (no field key)",
			err:  ValidationError{Kind: ErrCompleteness, Reason: "endorsed_names must be non-empty"},
			want: `{"Completeness":{"reason":"endorsed_names must be non-empty"}}`,
		},
		{
			name: "consistency only carries reason",
			err:  ValidationError{Kind: ErrConsistency, Reason: "candidate_id W-1 not in roster"},
			want: `{"Consistency":{"reason":"candidate_id W-1 not in roster"}}`,
		},
		{
			name: "policy only carries reason",
			err:  ValidationError{Kind: ErrPolicy, Reason: "client C-1 is on candidate's blacklist"},
			want: `{"Policy":{"reason":"client C-1 is on candidate's blacklist"}}`,
		},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			got, err := json.Marshal(c.err)
			if err != nil {
				t.Fatalf("marshal: %v", err)
			}
			if string(got) != c.want {
				t.Errorf("\n got: %s\nwant: %s", got, c.want)
			}
		})
	}
}

// TestValidationError_RoundTripsRustShape confirms a marshal+unmarshal
// returns the same fields. Important: a Go side reading a Rust-written
// ValidationError JSONL must populate Kind/Field/Reason correctly.
func TestValidationError_RoundTripsRustShape(t *testing.T) {
	original := ValidationError{Kind: ErrSchema, Field: "operation", Reason: "expected fill: prefix"}
	body, err := json.Marshal(original)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	var decoded ValidationError
	if err := json.Unmarshal(body, &decoded); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if decoded.Kind != original.Kind || decoded.Field != original.Field || decoded.Reason != original.Reason {
		t.Errorf("round-trip drift:\n  in: %+v\n out: %+v", original, decoded)
	}
}

// TestValidationError_ParsesLegacyFlatShape ensures consumers reading
// older Go-emitted JSONL still parse. This is the migration path for
// any persisted error rows that pre-date the 2026-05-02 alignment.
func TestValidationError_ParsesLegacyFlatShape(t *testing.T) {
	legacy := []byte(`{"Kind":"schema","Field":"x","Reason":"y"}`)
	var ve ValidationError
	if err := json.Unmarshal(legacy, &ve); err != nil {
		t.Fatalf("legacy shape should parse: %v", err)
	}
	if ve.Kind != ErrSchema || ve.Field != "x" || ve.Reason != "y" {
		t.Errorf("legacy parse wrong: %+v", ve)
	}
}

// TestValidationError_RejectsUnknownVariant catches drift if Rust adds
// a new variant the Go side hasn't learned about yet — surfaces as a
// parse error rather than a silent default.
func TestValidationError_RejectsUnknownVariant(t *testing.T) {
	body := []byte(`{"NewVariant":{"reason":"something"}}`)
	var ve ValidationError
	if err := json.Unmarshal(body, &ve); err == nil {
		t.Errorf("expected error on unknown variant, got %+v", ve)
	}
}
