package packs

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"testing"
)

// TestCanonicalProjectionJSON verifies that canonicalProjectionJSON normalizes
// json.RawMessage fields so that whitespace-only differences do not cause drift.
// Regression test for #1806.
func TestCanonicalProjectionJSON(t *testing.T) {
	tests := []struct {
		name     string
		input    any
		wantJSON string
	}{
		{
			name:     "compact object",
			input:    map[string]any{"a": 1, "b": 2},
			wantJSON: `{"a":1,"b":2}`,
		},
		{
			name: "struct with RawMessage",
			input: struct {
				Name  string          `json:"name"`
				Extra json.RawMessage `json:"extra"`
			}{
				Name:  "test",
				Extra: json.RawMessage(`{  "key"  :  "value"  }`),
			},
			wantJSON: `{"extra":{"key":"value"},"name":"test"}`,
		},
		{
			name: "struct with map of RawMessage",
			input: struct {
				Fields map[string]json.RawMessage `json:"fields"`
			}{
				Fields: map[string]json.RawMessage{
					"f1": json.RawMessage(`{  "nested"  :  true  }`),
					"f2": json.RawMessage(`[  1  ,  2  ,  3  ]`),
				},
			},
			wantJSON: `{"fields":{"f1":{"nested":true},"f2":[1,2,3]}}`,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got, err := canonicalProjectionJSON(tc.input)
			if err != nil {
				t.Fatalf("canonicalProjectionJSON: %v", err)
			}
			if string(got) != tc.wantJSON {
				t.Errorf("canonicalProjectionJSON:\ngot:  %s\nwant: %s", got, tc.wantJSON)
			}
		})
	}
}

// TestVerifyPackComponentDefault_RawMessageDrift verifies that the default
// verifier does not report drift when json.RawMessage fields differ only in
// whitespace formatting. Regression test for #1806.
func TestVerifyPackComponentDefault_RawMessageDrift(t *testing.T) {
	type projectionWithRawMessage struct {
		Name  string          `json:"name"`
		Extra json.RawMessage `json:"extra"`
	}

	// Expected has formatted JSON (e.g., from manifest)
	expected := projectionWithRawMessage{
		Name: "test",
		Extra: json.RawMessage(`{
  "key": "value",
  "nested": {
    "a": 1
  }
}`),
	}

	// Live has compact JSON (e.g., from DB round-trip)
	live := projectionWithRawMessage{
		Name:  "test",
		Extra: json.RawMessage(`{"key":"value","nested":{"a":1}}`),
	}

	// Canonicalize both
	expectedCanonical, err := canonicalProjectionJSON(expected)
	if err != nil {
		t.Fatalf("canonicalize expected: %v", err)
	}
	liveCanonical, err := canonicalProjectionJSON(live)
	if err != nil {
		t.Fatalf("canonicalize live: %v", err)
	}

	// They must be equal despite different RawMessage bytes
	if string(expectedCanonical) != string(liveCanonical) {
		t.Errorf("false drift detected:\nexpected: %s\nlive:     %s", expectedCanonical, liveCanonical)
	}
}

// TestVerifyPackComponentDefault_RealDrift verifies that the default verifier
// still detects real projection drift (different values, not just whitespace).
func TestVerifyPackComponentDefault_RealDrift(t *testing.T) {
	type projectionWithRawMessage struct {
		Name  string          `json:"name"`
		Extra json.RawMessage `json:"extra"`
	}

	expected := projectionWithRawMessage{
		Name:  "test",
		Extra: json.RawMessage(`{"key":"value1"}`),
	}

	live := projectionWithRawMessage{
		Name:  "test",
		Extra: json.RawMessage(`{"key":"value2"}`),
	}

	expectedCanonical, err := canonicalProjectionJSON(expected)
	if err != nil {
		t.Fatalf("canonicalize expected: %v", err)
	}
	liveCanonical, err := canonicalProjectionJSON(live)
	if err != nil {
		t.Fatalf("canonicalize live: %v", err)
	}

	// They must differ — this is real drift
	if string(expectedCanonical) == string(liveCanonical) {
		t.Errorf("real drift not detected:\nexpected: %s\nlive:     %s", expectedCanonical, liveCanonical)
	}
}

// TestCanonicalProjectionJSON_Int64Precision verifies that canonicalProjectionJSON
// preserves exact int64 values above 2^53 (where float64 loses precision).
// This is critical for projections containing unbounded int64 fields such as
// sourceBackedAutoNumberFormatRow.Start.
// Regression test for Codex Review BLOCK: float64 round-trip aliases large int64 values.
func TestCanonicalProjectionJSON_Int64Precision(t *testing.T) {
	// 2^53 + 1 = 9007199254740993, which cannot be represented exactly in float64
	const largeInt64 int64 = 9007199254740993

	type projectionWithInt64 struct {
		Name  string `json:"name"`
		Start int64  `json:"start"`
	}

	input := projectionWithInt64{
		Name:  "test",
		Start: largeInt64,
	}

	canonical, err := canonicalProjectionJSON(input)
	if err != nil {
		t.Fatalf("canonicalProjectionJSON: %v", err)
	}

	// The canonical JSON must contain the exact int64 value, not a float64-aliased approximation
	want := `{"name":"test","start":9007199254740993}`
	if string(canonical) != want {
		t.Errorf("int64 precision lost:\ngot:  %s\nwant: %s", canonical, want)
	}

	// Verify that two projections with the same large int64 are equal
	expected := projectionWithInt64{Name: "test", Start: largeInt64}
	live := projectionWithInt64{Name: "test", Start: largeInt64}

	expectedCanonical, err := canonicalProjectionJSON(expected)
	if err != nil {
		t.Fatalf("canonicalize expected: %v", err)
	}
	liveCanonical, err := canonicalProjectionJSON(live)
	if err != nil {
		t.Fatalf("canonicalize live: %v", err)
	}

	if string(expectedCanonical) != string(liveCanonical) {
		t.Errorf("same int64 values should canonicalize equal:\nexpected: %s\nlive:     %s", expectedCanonical, liveCanonical)
	}
}

// TestRejectDuplicateJSONKeys verifies that rejectDuplicateJSONKeys detects
// duplicate keys at any nesting depth.
// Regression test for IANTHEREAL P1: duplicate JSON keys could hide drift
// in locked pack-owned RawMessage fields.
func TestRejectDuplicateJSONKeys(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		wantErr bool
	}{
		{"empty object", `{}`, false},
		{"simple object", `{"a":1,"b":2}`, false},
		{"nested object no dups", `{"a":{"b":1},"c":2}`, false},
		{"duplicate top-level keys", `{"a":1,"b":2,"a":3}`, true},
		{"duplicate nested keys", `{"a":{"x":1,"x":2}}`, true},
		{"duplicate in array element", `[{"a":1},{"a":1,"a":2}]`, true},
		{"same key at different levels is ok", `{"a":{"a":1}}`, false},
		{"array with no dups", `[1,2,3]`, false},
		{"primitive", `42`, false},
		{"string", `"hello"`, false},
		{"null", `null`, false},
		{"complex nested", `{"x":{"a":1},"y":{"a":2},"z":[{"a":1}]}`, false},
		{"duplicate in deep nesting", `{"x":{"y":{"z":1,"z":2}}}`, true},
		{"escaped string key", `{"a\\b":1,"a\\b":2}`, true},
		{"whitespace variations", `{ "a" : 1 , "a" : 2 }`, true},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			err := rejectDuplicateJSONKeys([]byte(tc.input), "test")
			if tc.wantErr && err == nil {
				t.Errorf("rejectDuplicateJSONKeys(%q): expected error, got nil", tc.input)
			}
			if !tc.wantErr && err != nil {
				t.Errorf("rejectDuplicateJSONKeys(%q): unexpected error: %v", tc.input, err)
			}
		})
	}
}

// TestCanonicalProjectionJSON_DuplicateKeys verifies that canonicalProjectionJSON
// fails closed when the marshaled projection contains duplicate JSON keys.
// This prevents a tampered RawMessage from hiding real drift via last-wins dedup.
func TestCanonicalProjectionJSON_DuplicateKeys(t *testing.T) {
	type projectionWithRaw struct {
		Name  string          `json:"name"`
		Extra json.RawMessage `json:"extra"`
	}

	// RawMessage with duplicate keys — this should fail closed
	input := projectionWithRaw{
		Name:  "test",
		Extra: json.RawMessage(`{"a":1,"a":2}`),
	}

	_, err := canonicalProjectionJSON(input)
	if err == nil {
		t.Errorf("canonicalProjectionJSON should fail on duplicate JSON keys")
	}
}

// TestSourceBackedCanonicalJSON_Int64Precision verifies that sourceBackedCanonicalJSON
// preserves exact int64 values above 2^53 via UseNumber().
// Regression test for IANTHEREAL P1: sourceBackedCanonicalJSON previously used
// plain json.Unmarshal which decodes numbers as float64, causing values like
// 9007199254740992 and 9007199254740993 to collapse to the same float64.
func TestSourceBackedCanonicalJSON_Int64Precision(t *testing.T) {
	// Two values that differ only in the last bit above 2^53
	a := []byte(`{"start":9007199254740992}`)
	b := []byte(`{"start":9007199254740993}`)

	canonA, err := sourceBackedCanonicalJSON(a)
	if err != nil {
		t.Fatalf("sourceBackedCanonicalJSON(a): %v", err)
	}
	canonB, err := sourceBackedCanonicalJSON(b)
	if err != nil {
		t.Fatalf("sourceBackedCanonicalJSON(b): %v", err)
	}

	if string(canonA) == string(canonB) {
		t.Errorf("sourceBackedCanonicalJSON collapsed distinct int64 values:\na: %s\ncanonA: %s\nb: %s\ncanonB: %s",
			a, canonA, b, canonB)
	}
}

// TestSourceBackedJSONEqualString_Int64Precision verifies that the full
// sourceBackedJSONEqualString comparison path preserves int64 precision.
func TestSourceBackedJSONEqualString_Int64Precision(t *testing.T) {
	got := sql.NullString{String: `{"start":9007199254740993}`, Valid: true}
	want := json.RawMessage(`{"start":9007199254740993}`)

	if !sourceBackedJSONEqualString(got, want) {
		t.Errorf("sourceBackedJSONEqualString: identical int64 values should be equal")
	}

	// Different int64 values above 2^53 must NOT compare equal
	got2 := sql.NullString{String: `{"start":9007199254740992}`, Valid: true}
	want2 := json.RawMessage(`{"start":9007199254740993}`)

	if sourceBackedJSONEqualString(got2, want2) {
		t.Errorf("sourceBackedJSONEqualString: distinct int64 values must not be equal")
	}
}

// TestSourceBackedCanonicalJSON_DuplicateKeys verifies that sourceBackedCanonicalJSON
// fails closed on duplicate JSON keys.
func TestSourceBackedCanonicalJSON_DuplicateKeys(t *testing.T) {
	input := []byte(`{"a":1,"b":2,"a":3}`)
	_, err := sourceBackedCanonicalJSON(input)
	if err == nil {
		t.Errorf("sourceBackedCanonicalJSON: should reject duplicate keys")
	}
}

// TestCanonicalComponentPayloadForHash_Int64Precision verifies that
// canonicalComponentPayloadForHash preserves exact int64 representation.
func TestCanonicalComponentPayloadForHash_Int64Precision(t *testing.T) {
	payload := []byte(`{"start":9007199254740993}`)
	result, err := canonicalComponentPayloadForHash(payload)
	if err != nil {
		t.Fatalf("canonicalComponentPayloadForHash: %v", err)
	}
	expected := `{"start":9007199254740993}`
	if result != expected {
		t.Errorf("canonicalComponentPayloadForHash: expected %s, got %s", expected, result)
	}
}

// TestCanonicalComponentPayloadForHash_DuplicateKeys verifies that
// canonicalComponentPayloadForHash fails closed on duplicate JSON keys.
func TestCanonicalComponentPayloadForHash_DuplicateKeys(t *testing.T) {
	payload := []byte(`{"a":1,"b":2,"a":3}`)
	_, err := canonicalComponentPayloadForHash(payload)
	if err == nil {
		t.Errorf("canonicalComponentPayloadForHash: should reject duplicate keys")
	}
}

// TestCanonicalComponentPayloadForHash_NonJSONFallback verifies that
// canonicalComponentPayloadForHash falls back to line-ending normalization
// when payload is not valid JSON.
func TestCanonicalComponentPayloadForHash_NonJSONFallback(t *testing.T) {
	payload := []byte("not json\r\nwith\rwindows\r\nendings")
	result, err := canonicalComponentPayloadForHash(payload)
	if err != nil {
		t.Fatalf("canonicalComponentPayloadForHash: %v", err)
	}
	expected := "not json\nwith\nwindows\nendings"
	if result != expected {
		t.Errorf("canonicalComponentPayloadForHash: expected %q, got %q", expected, result)
	}
}

// TestConnectorCalloutProfileVerifyComponent_RawMessageDrift verifies that
// connector callout profile verify does not report drift when BatchSpec.ExtraBodyFields
// contains formatted JSON in the manifest but compact JSON in the DB.
// Regression test for #1806.
func TestConnectorCalloutProfileVerifyComponent_RawMessageDrift(t *testing.T) {
	// Expected projection with formatted ExtraBodyFields (from manifest)
	expected := connectorCalloutProfileProjectionRow{
		ProfileName:   "test-profile",
		CredentialRef: "test-cred",
		EndpointClass: "test-endpoint",
		RetryPolicy:   connectorCalloutProfileRetryPolicy{MaxAttempts: 3},
		BatchSpec: connectorCalloutProfileBatchSpec{
			MaxBatchSize:    100,
			IDsRequestField: "ids",
			ExtraBodyFields: map[string]json.RawMessage{
				"field1": json.RawMessage(`{
  "nested": {
    "key": "value"
  }
}`),
			},
		},
	}

	// Live projection with compact ExtraBodyFields (from DB round-trip)
	live := connectorCalloutProfileProjectionRow{
		ProfileName:   "test-profile",
		CredentialRef: "test-cred",
		EndpointClass: "test-endpoint",
		RetryPolicy:   connectorCalloutProfileRetryPolicy{MaxAttempts: 3},
		BatchSpec: connectorCalloutProfileBatchSpec{
			MaxBatchSize:    100,
			IDsRequestField: "ids",
			ExtraBodyFields: map[string]json.RawMessage{
				"field1": json.RawMessage(`{"nested":{"key":"value"}}`),
			},
		},
	}

	// Canonicalize both
	expectedCanonical, err := canonicalProjectionJSON(expected)
	if err != nil {
		t.Fatalf("canonicalize expected: %v", err)
	}
	liveCanonical, err := canonicalProjectionJSON(live)
	if err != nil {
		t.Fatalf("canonicalize live: %v", err)
	}

	// They must be equal despite different RawMessage bytes
	if string(expectedCanonical) != string(liveCanonical) {
		t.Errorf("false drift on ExtraBodyFields:\nexpected: %s\nlive:     %s", expectedCanonical, liveCanonical)
	}
}

// mockPackComponent is a minimal PackComponent implementation for testing
// verifyPackComponentDefault without a real database.
type mockPackComponent struct {
	kind         ComponentKind
	businessKey  string
	expectedProj any
	liveProj     any
	liveErr      error
}

func (m *mockPackComponent) Kind() ComponentKind          { return m.kind }
func (m *mockPackComponent) PackName() string             { return "test-pack" }
func (m *mockPackComponent) BusinessKey() string          { return m.businessKey }
func (m *mockPackComponent) Manageability() Manageability { return ManageabilityLocked }
func (m *mockPackComponent) ExpectedPayload() ([]byte, error) {
	return json.Marshal(m.expectedProj)
}
func (m *mockPackComponent) ExpectedProjection() (any, error) {
	return m.expectedProj, nil
}
func (m *mockPackComponent) LiveProjection(ctx context.Context, deps VerifyDeps) (any, error) {
	return m.liveProj, m.liveErr
}
func (m *mockPackComponent) Apply(ctx context.Context, deps ApplyDeps) error {
	return nil
}

// TestVerifyPackComponentDefault_Integration verifies verifyPackComponentDefault
// end-to-end with a mock component that has json.RawMessage fields.
func TestVerifyPackComponentDefault_Integration(t *testing.T) {
	type projection struct {
		Name  string          `json:"name"`
		Extra json.RawMessage `json:"extra"`
	}

	comp := &mockPackComponent{
		kind:        ComponentKindRecordType,
		businessKey: "test-rt",
		expectedProj: projection{
			Name:  "test",
			Extra: json.RawMessage(`{  "key"  :  "value"  }`),
		},
		liveProj: projection{
			Name:  "test",
			Extra: json.RawMessage(`{"key":"value"}`),
		},
	}

	ctx := context.Background()
	deps := VerifyDeps{}

	err := verifyPackComponentDefault(ctx, deps, comp)
	if err != nil {
		t.Errorf("verifyPackComponentDefault reported false drift: %v", err)
	}
}

// TestVerifyPackComponentDefault_RealDriftIntegration verifies that
// verifyPackComponentDefault still detects real drift.
func TestVerifyPackComponentDefault_RealDriftIntegration(t *testing.T) {
	type projection struct {
		Name  string          `json:"name"`
		Extra json.RawMessage `json:"extra"`
	}

	comp := &mockPackComponent{
		kind:        ComponentKindRecordType,
		businessKey: "test-rt",
		expectedProj: projection{
			Name:  "test",
			Extra: json.RawMessage(`{"key":"value1"}`),
		},
		liveProj: projection{
			Name:  "test",
			Extra: json.RawMessage(`{"key":"value2"}`),
		},
	}

	ctx := context.Background()
	deps := VerifyDeps{}

	err := verifyPackComponentDefault(ctx, deps, comp)
	if err == nil {
		t.Errorf("verifyPackComponentDefault did not detect real drift")
	}
}

// TestVerifyPackComponentDefault_LiveError verifies that verifyPackComponentDefault
// propagates errors from LiveProjection.
func TestVerifyPackComponentDefault_LiveError(t *testing.T) {
	comp := &mockPackComponent{
		kind:         ComponentKindRecordType,
		businessKey:  "test-rt",
		expectedProj: map[string]any{"name": "test"},
		liveErr:      fmt.Errorf("db connection failed"),
	}

	ctx := context.Background()
	deps := VerifyDeps{}

	err := verifyPackComponentDefault(ctx, deps, comp)
	if err == nil {
		t.Errorf("verifyPackComponentDefault did not propagate LiveProjection error")
	}
}
