package plan

import (
	"errors"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestNormalizePartialInputJSONStrictAndNormalized(t *testing.T) {
	contract := testInputContract()
	input, err := NormalizePartialInputJSON(contract, []byte(`{
  "when":"2026-07-21T09:00:00+08:00",
  "timeout":"PT1S",
  "ratio":1.2300e2,
  "metadata":{"z":1e0,"a":true},
  "tags":["a","b"]
}`))
	require.NoError(t, err)
	payload, err := input.DeterministicJSON()
	require.NoError(t, err)
	require.JSONEq(t, `{
  "metadata":{"a":true,"z":1},
  "ratio":123,
  "tags":["a","b"],
  "timeout":1000000000,
  "when":"2026-07-21T01:00:00Z"
}`, string(payload))
	require.Equal(t, []string{"metadata", "ratio", "tags", "timeout", "when"}, input.Keys())
}

func TestNormalizePartialInputJSONRejectsUnknownDuplicateAndType(t *testing.T) {
	contract := testInputContract()
	tests := []struct {
		name string
		raw  string
		code string
		path string
	}{
		{name: "unknown", raw: `{"other":1}`, code: "input.unknown_field", path: "$.other"},
		{name: "duplicate", raw: `{"ratio":1,"ratio":2}`, code: "input.duplicate_field", path: "$.ratio"},
		{name: "nested duplicate", raw: `{"metadata":{"x":1,"x":2}}`, code: "input.duplicate_field", path: "$.metadata.x"},
		{name: "wrong type", raw: `{"enabled":"yes"}`, code: "input.type", path: "$.enabled"},
		{name: "enum", raw: `{"mode":"other"}`, code: "input.enum", path: "$.mode"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, err := NormalizePartialInputJSON(contract, []byte(test.raw))
			require.Error(t, err)
			var inputError InputError
			require.True(t, errors.As(err, &inputError))
			require.Equal(t, test.code, inputError.Code)
			require.Equal(t, test.path, inputError.Path)
		})
	}
}

func TestNormalizePartialInputJSONUsesFiniteRuntimeNumberDomain(t *testing.T) {
	input, err := NormalizePartialInputJSON(testInputContract(), []byte(`{
		"ratio":9007199254740993,
		"metadata":{"rounded":123456789012345678901}
	}`))
	require.NoError(t, err)
	payload, err := input.DeterministicJSON()
	require.NoError(t, err)
	require.JSONEq(t, `{
		"ratio":9007199254740992,
		"metadata":{"rounded":123456789012345680000}
	}`, string(payload))

	_, err = NormalizePartialInputJSON(testInputContract(), []byte(`{"ratio":1e4095}`))
	require.Error(t, err)
	var inputError InputError
	require.True(t, errors.As(err, &inputError))
	require.Equal(t, "input.number", inputError.Code)
	require.Equal(t, "$.ratio", inputError.Path)

	_, err = NormalizePartialInputJSON(testInputContract(), []byte(`{"metadata":{"overflow":1e4095}}`))
	require.Error(t, err)
	require.True(t, errors.As(err, &inputError))
	require.Equal(t, "input.object", inputError.Code)
}

func TestFinalizeInputAppliesDefaultsAndLayerPrecedence(t *testing.T) {
	contract := testInputContract()
	bindingDefaults, err := NormalizePartialInputJSON(contract, []byte(`{"title":"binding","ratio":1}`))
	require.NoError(t, err)
	occurrence, err := NormalizePartialInputJSON(contract, []byte(`{"title":"request","ratio":2}`))
	require.NoError(t, err)
	constants, err := NormalizePartialInputJSON(contract, []byte(`{"ratio":3}`))
	require.NoError(t, err)

	final, err := FinalizeInput(contract, bindingDefaults, occurrence, constants)
	require.NoError(t, err)
	payload, err := final.DeterministicJSON()
	require.NoError(t, err)
	require.JSONEq(t, `{"enabled":true,"title":"request","ratio":3}`, string(payload))

	_, err = NormalizeFinalInputJSON(contract, []byte(`{}`))
	require.Error(t, err)
	var inputError InputError
	require.True(t, errors.As(err, &inputError))
	require.Equal(t, "input.required", inputError.Code)
	require.Equal(t, "$.title", inputError.Path)
	require.Equal(t, []string{"title"}, MissingRequiredInputs(contract))
	require.Empty(t, MissingRequiredInputs(contract, bindingDefaults))
}

func TestFinalizeInputEnforcesAggregateRuntimeBudgetAcrossLayers(t *testing.T) {
	t.Run("value count", func(t *testing.T) {
		contract := ArtifactContract{
			{Name: "left", Schema: ArtifactSchema{Type: TypeObject, Values: []string{}}},
			{Name: "right", Schema: ArtifactSchema{Type: TypeObject, Values: []string{}}},
		}
		values := strings.TrimSuffix(strings.Repeat("0,", 5000), ",")
		left, err := NormalizePartialInputJSON(contract, []byte(`{"left":{"values":[`+values+`]}}`))
		require.NoError(t, err)
		right, err := NormalizePartialInputJSON(contract, []byte(`{"right":{"values":[`+values+`]}}`))
		require.NoError(t, err)

		_, err = FinalizeInput(contract, left, right)
		require.Error(t, err)
		var inputError InputError
		require.ErrorAs(t, err, &inputError)
		require.Equal(t, "input.budget", inputError.Code)
		require.Contains(t, inputError.Message, "8192")
	})

	t.Run("normalized bytes", func(t *testing.T) {
		contract := ArtifactContract{
			{Name: "left", Schema: ArtifactSchema{Type: TypeString, Values: []string{}}},
			{Name: "right", Schema: ArtifactSchema{Type: TypeString, Values: []string{}}},
		}
		value := strings.Repeat("x", MaxInputBytes/2+1024)
		left, err := NormalizePartialInputJSON(contract, []byte(`{"left":"`+value+`"}`))
		require.NoError(t, err)
		right, err := NormalizePartialInputJSON(contract, []byte(`{"right":"`+value+`"}`))
		require.NoError(t, err)

		_, err = FinalizeInput(contract, left, right)
		require.Error(t, err)
		var inputError InputError
		require.ErrorAs(t, err, &inputError)
		require.Equal(t, "input.budget", inputError.Code)
		require.Contains(t, inputError.Message, "bytes")
	})
}

func testInputContract() ArtifactContract {
	defaultEnabled := ArtifactLiteral(`true`)
	return ArtifactContract{
		{Name: "enabled", Schema: ArtifactSchema{Type: TypeBoolean, Default: &defaultEnabled, Values: []string{}}},
		{Name: "metadata", Schema: ArtifactSchema{Type: TypeObject, Values: []string{}}},
		{Name: "mode", Schema: ArtifactSchema{Type: TypeEnum, Values: []string{"fast", "safe"}}},
		{Name: "ratio", Schema: ArtifactSchema{Type: TypeNumber, Values: []string{}}},
		{Name: "tags", Schema: ArtifactSchema{Type: TypeArray, Values: []string{}, Items: &ArtifactSchema{Type: TypeString, Values: []string{}}}},
		{Name: "timeout", Schema: ArtifactSchema{Type: TypeDuration, Values: []string{}}},
		{Name: "title", Schema: ArtifactSchema{Type: TypeString, Required: true, Values: []string{}}},
		{Name: "when", Schema: ArtifactSchema{Type: TypeDatetime, Values: []string{}}},
	}
}
