package ldmodel

import (
	"encoding/json"

	"github.com/launchdarkly/go-sdk-common/v3/ldattr"
	"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
	"github.com/launchdarkly/go-sdk-common/v3/ldtime"
	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
)

type flagSerializationTestParams struct {
	name          string
	flag          FeatureFlag
	jsonString    string   // for marshaling tests, jsonString doesn't need to include any of flagTopDefaultLevelProperties
	jsonAltInputs []string // if specified, unmarshaling test will verify that these parse to the same result

	isCustomClientSideAvailability bool
}

type segmentSerializationTestParams struct {
	name          string
	segment       Segment
	jsonString    string   // for marshaling tests, jsonString doesn't need to include any of segmentTopDefaultLevelProperties
	jsonAltInputs []string // if specified, unmarshaling test will verify that these parse to the same result
}

type clauseSerializationTestParams struct {
	name          string
	clause        Clause
	jsonString    string
	jsonAltInputs []string
}

type rolloutSerializationTestParams struct {
	name          string
	rollout       Rollout
	jsonString    string
	jsonAltInputs []string
}

var flagTopLevelDefaultProperties = map[string]interface{}{
	"key":                    "",
	"deleted":                false,
	"variations":             []interface{}{},
	"on":                     false,
	"offVariation":           nil,
	"fallthrough":            map[string]interface{}{},
	"targets":                []interface{}{},
	"contextTargets":         []interface{}{},
	"prerequisites":          []interface{}{},
	"rules":                  []interface{}{},
	"clientSide":             false,
	"trackEvents":            false,
	"trackEventsFallthrough": false,
	"debugEventsUntilDate":   nil,
	"salt":                   "",
	"version":                0,
}

var segmentTopLevelDefaultProperties = map[string]interface{}{
	"key":              "",
	"deleted":          false,
	"version":          0,
	"generation":       nil,
	"included":         []string{},
	"excluded":         []string{},
	"includedContexts": []interface{}{},
	"excludedContexts": []interface{}{},
	"rules":            []interface{}{},
	"salt":             "",
}

func makeFlagSerializationTestParams() []flagSerializationTestParams {
	ret := []flagSerializationTestParams{
		{
			name:       "defaults",
			flag:       FeatureFlag{},
			jsonString: `{}`,
			jsonAltInputs: []string{
				`{"deleted": false}`,
				`{"on": false}`,
				`{"offVariation": null}`,
				`{"fallthrough": {"variation": null}}`,
				`{"prerequisites": []}`,
				`{"targets": []}`,
				`{"rules": []}`,
			},
		},
		{
			name:       "key",
			flag:       FeatureFlag{Key: "flag-key"},
			jsonString: `{"key": "flag-key"}`,
		},
		{
			name:       "version",
			flag:       FeatureFlag{Version: 99},
			jsonString: `{"version": 99}`,
		},
		{
			name:       "deleted",
			flag:       FeatureFlag{Deleted: true},
			jsonString: `{"deleted": true}`,
		},
		{
			name: "variations",
			flag: FeatureFlag{Variations: []ldvalue.Value{
				ldvalue.Bool(true),
				ldvalue.Int(1),
				ldvalue.Float64(1.5),
				ldvalue.String("x"),
				ldvalue.ArrayOf(),
				ldvalue.ObjectBuild().Build(),
			},
			},
			jsonString: `{"variations": [true, 1, 1.5, "x", [], {}]}`,
		},
		{
			name:       "on",
			flag:       FeatureFlag{On: true},
			jsonString: `{"on": true}`,
		},
		{
			name:       "offVariation",
			flag:       FeatureFlag{OffVariation: ldvalue.NewOptionalInt(1)},
			jsonString: `{"offVariation": 1}`,
		},
		{
			name:          "fallthrough variation",
			flag:          FeatureFlag{Fallthrough: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)}},
			jsonString:    `{"fallthrough": {"variation": 1}}`,
			jsonAltInputs: []string{`{"fallthrough": {"variation": 1, "rollout": null}}`},
		},
		{
			name: "prerequisites",
			flag: FeatureFlag{
				Prerequisites: []Prerequisite{
					{Variation: 1, Key: "pre-key"},
				},
			},
			jsonString: `{"prerequisites": [ {"variation": 1, "key": "pre-key"} ]}`,
		},
		{
			name: "targets",
			flag: FeatureFlag{
				Targets: []Target{
					{Variation: 1, Values: []string{"a", "b"}},
				},
			},
			jsonString: `{"targets": [ {"variation": 1, "values": ["a", "b"]} ]}`,
		},
		{
			name: "contextTargets",
			flag: FeatureFlag{
				ContextTargets: []Target{
					{ContextKind: "org", Variation: 1, Values: []string{"a", "b"}},
				},
			},
			jsonString: `{"contextTargets": [ {"contextKind": "org", "variation": 1, "values": ["a", "b"]} ]}`,
		},
		{
			name: "minimal rule with variation",
			flag: FeatureFlag{
				Rules: []FlagRule{
					{VariationOrRollout: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)}},
				},
			},
			jsonString:    `{"rules": [ {"variation": 1, "clauses": [], "trackEvents": false} ]}`,
			jsonAltInputs: []string{`{"rules": [ {"variation": 1} ]}`},
		},
		{
			name: "rule ID",
			flag: FeatureFlag{
				Rules: []FlagRule{
					{ID: "a", VariationOrRollout: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)}},
				},
			},
			jsonString: `{"rules": [ {"id": "a", "variation": 1, "clauses": [], "trackEvents": false} ]}`,
		},
		{
			name: "rule trackEvents",
			flag: FeatureFlag{
				Rules: []FlagRule{
					{VariationOrRollout: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)}, TrackEvents: true},
				},
			},
			jsonString: `{"rules": [ {"variation": 1, "clauses": [], "trackEvents": true} ]}`,
		},
		{
			name: "clientSide",
			flag: FeatureFlag{
				ClientSideAvailability: ClientSideAvailability{
					UsingMobileKey:     true,
					UsingEnvironmentID: true,
					Explicit:           false,
				},
			},
			jsonString:                     `{"clientSide": true}`,
			isCustomClientSideAvailability: true,
		},
		{
			name: "clientSide explicitly false",
			flag: FeatureFlag{
				ClientSideAvailability: ClientSideAvailability{
					UsingMobileKey:     true,
					UsingEnvironmentID: false,
					Explicit:           false,
				},
			},
			jsonString:                     `{"clientSide": false}`,
			isCustomClientSideAvailability: true,
		},
		{
			name: "clientSideAvailability both false",
			flag: FeatureFlag{
				ClientSideAvailability: ClientSideAvailability{
					Explicit: true,
				},
			},
			jsonString:                     `{"clientSideAvailability": {"usingMobileKey": false, "usingEnvironmentId": false}}`,
			isCustomClientSideAvailability: true,
		},
		{
			name: "clientSideAvailability both true",
			flag: FeatureFlag{
				ClientSideAvailability: ClientSideAvailability{
					UsingMobileKey:     true,
					UsingEnvironmentID: true,
					Explicit:           true,
				},
			},
			jsonString:                     `{"clientSide": true, "clientSideAvailability": {"usingMobileKey": true, "usingEnvironmentId": true}}`,
			isCustomClientSideAvailability: true,
		},
		{
			name: "clientSideAvailability usingMobileKey only",
			flag: FeatureFlag{
				ClientSideAvailability: ClientSideAvailability{
					UsingMobileKey: true,
					Explicit:       true,
				},
			},
			jsonString:                     `{"clientSideAvailability": {"usingMobileKey": true, "usingEnvironmentId": false}}`,
			isCustomClientSideAvailability: true,
		},
		{
			name: "clientSideAvailability usingEnvironmentId only",
			flag: FeatureFlag{
				ClientSideAvailability: ClientSideAvailability{
					UsingEnvironmentID: true,
					Explicit:           true,
				},
			},
			jsonString:                     `{"clientSide": true, "clientSideAvailability": {"usingMobileKey": false, "usingEnvironmentId": true}}`,
			isCustomClientSideAvailability: true,
		},
		{
			name:       "salt",
			flag:       FeatureFlag{Salt: "flag-salt"},
			jsonString: `{"salt": "flag-salt"}`,
		},
		{
			name:       "trackEvents",
			flag:       FeatureFlag{TrackEvents: true},
			jsonString: `{"trackEvents": true}`,
		},
		{
			name:       "trackEventsFallthrough",
			flag:       FeatureFlag{TrackEventsFallthrough: true},
			jsonString: `{"trackEventsFallthrough": true}`,
		},
		{
			name:       "debugEventsUntilDate",
			flag:       FeatureFlag{DebugEventsUntilDate: ldtime.UnixMillisecondTime(1000)},
			jsonString: `{"debugEventsUntilDate": 1000}`,
		},
	}

	makeFlagJSONForClause := func(clauseJSON string) string {
		return `{"rules": [ {"variation": 1, "clauses": [` + clauseJSON + `], "trackEvents": false }]}`
	}
	for _, cp := range makeClauseSerializationTestParams() {
		fp := flagSerializationTestParams{
			name: "rule clause " + cp.name,
			flag: FeatureFlag{
				Rules: []FlagRule{
					{
						VariationOrRollout: VariationOrRollout{Variation: ldvalue.NewOptionalInt(1)},
						Clauses:            []Clause{cp.clause},
					},
				},
			},
			jsonString: makeFlagJSONForClause(cp.jsonString),
		}
		for _, alt := range cp.jsonAltInputs {
			fp.jsonAltInputs = append(fp.jsonAltInputs, makeFlagJSONForClause(alt))
		}
		ret = append(ret, fp)
	}

	makeFlagJSONForFallthroughRollout := func(rolloutJSON string) string {
		return `{"fallthrough": {"rollout": ` + rolloutJSON + `}}`
	}
	makeFlagJSONForRuleRollout := func(rolloutJSON string) string {
		return `{"rules": [ {"rollout": ` + rolloutJSON + `, "clauses": [], "trackEvents": false} ]}`
	}
	for _, rp := range makeRolloutSerializationTestParams() {
		fp1 := flagSerializationTestParams{
			name: "fallthrough rollout " + rp.name,
			flag: FeatureFlag{
				Fallthrough: VariationOrRollout{Rollout: rp.rollout},
			},
			jsonString: makeFlagJSONForFallthroughRollout(rp.jsonString),
		}
		for _, alt := range rp.jsonAltInputs {
			fp1.jsonAltInputs = append(fp1.jsonAltInputs, makeFlagJSONForFallthroughRollout(alt))
		}
		fp2 := flagSerializationTestParams{
			name: "rule rollout " + rp.name,
			flag: FeatureFlag{
				Rules: []FlagRule{{VariationOrRollout: VariationOrRollout{Rollout: rp.rollout}}},
			},
			jsonString: makeFlagJSONForRuleRollout(rp.jsonString),
		}
		for _, alt := range rp.jsonAltInputs {
			fp2.jsonAltInputs = append(fp2.jsonAltInputs, makeFlagJSONForRuleRollout(alt))
		}
		ret = append(ret, fp1, fp2)
	}

	return ret
}

func makeSegmentSerializationTestParams() []segmentSerializationTestParams {
	ret := []segmentSerializationTestParams{
		{
			name:       "defaults",
			segment:    Segment{},
			jsonString: `{}`,
			jsonAltInputs: []string{
				`{"deleted": false}`,
				`{"included": []}`,
				`{"excluded": []}`,
				`{"rules": []}`,
				`{"unbounded": false}`,
				`{"generation": null}`,
			},
		},
		{
			name:       "key",
			segment:    Segment{Key: "segment-key"},
			jsonString: `{"key": "segment-key"}`,
		},
		{
			name:       "version",
			segment:    Segment{Version: 99},
			jsonString: `{"version": 99}`,
		},
		{
			name:       "deleted",
			segment:    Segment{Deleted: true},
			jsonString: `{"deleted": true}`,
		},
		{
			name:       "included",
			segment:    Segment{Included: []string{"a", "b"}},
			jsonString: `{"included": ["a", "b"]}`,
		},
		{
			name:       "excluded",
			segment:    Segment{Excluded: []string{"a", "b"}},
			jsonString: `{"excluded": ["a", "b"]}`,
		},
		{
			name: "includedContexts",
			segment: Segment{
				IncludedContexts: []SegmentTarget{
					{ContextKind: "org", Values: []string{"a", "b"}},
				}},
			jsonString: `{"includedContexts": [ {"contextKind": "org", "values": ["a", "b"]} ]}`,
		},
		{
			name: "excludedContexts",
			segment: Segment{
				ExcludedContexts: []SegmentTarget{
					{ContextKind: "org", Values: []string{"a", "b"}},
				}},
			jsonString: `{"excludedContexts": [ {"contextKind": "org", "values": ["a", "b"]} ]}`,
		},
		{
			name: "minimal rule",
			segment: Segment{
				Rules: []SegmentRule{
					{},
				},
			},
			jsonString:    `{"rules": [ {"id": "", "clauses": []} ]}`,
			jsonAltInputs: []string{`{"rules": [ {} ]}`},
		},
		{
			name: "minimal rule with weight",
			segment: Segment{
				Rules: []SegmentRule{
					{Weight: ldvalue.NewOptionalInt(100000)},
				},
			},
			jsonString: `{"rules": [ {"id": "", "weight": 100000, "clauses": []} ]}`,
		},
		{
			name: "rule bucketBy",
			segment: Segment{
				Rules: []SegmentRule{
					{Weight: ldvalue.NewOptionalInt(100000), BucketBy: ldattr.NewLiteralRef("name")},
				},
			},
			jsonString: `{"rules": [ {"id": "", "weight": 100000, "bucketBy": "name", "clauses": []} ]}`,
		},
		{
			name: "rule bucketBy invalid ref",
			// Here we verify that an invalid attribute ref doesn't make parsing fail and is preserved in reserialization
			segment: Segment{
				Rules: []SegmentRule{
					{
						RolloutContextKind: ldcontext.Kind("user"),
						Weight:             ldvalue.NewOptionalInt(100000),
						BucketBy:           ldattr.NewRef("///"),
					},
				},
			},
			jsonString: `{"rules": [ {"id": "", "weight": 100000, "rolloutContextKind": "user", "bucketBy": "///", "clauses": []} ]}`,
		},
		{
			name: "rule rolloutContextKind",
			segment: Segment{
				Rules: []SegmentRule{
					{Weight: ldvalue.NewOptionalInt(100000), RolloutContextKind: ldcontext.Kind("org")},
				},
			},
			jsonString: `{"rules": [ {"id": "", "weight": 100000, "rolloutContextKind": "org", "clauses": []} ]}`,
		},
		{
			name: "rule ID",
			segment: Segment{
				Rules: []SegmentRule{
					{ID: "a"},
				},
			},
			jsonString: `{"rules": [ {"id": "a", "clauses": []} ]}`,
		},
		{
			name:       "unbounded and generation",
			segment:    Segment{Unbounded: true, Generation: ldvalue.NewOptionalInt(1)},
			jsonString: `{"unbounded": true, "generation": 1}`,
		},
		{
			name:       "unbounded and generation and unboundedContextKind",
			segment:    Segment{Unbounded: true, UnboundedContextKind: "org", Generation: ldvalue.NewOptionalInt(1)},
			jsonString: `{"unbounded": true, "unboundedContextKind": "org", "generation": 1}`,
		},
		{
			name:       "salt",
			segment:    Segment{Salt: "segment-salt"},
			jsonString: `{"salt": "segment-salt"}`,
		},
	}

	makeSegmentJSONForClause := func(clauseJSON string) string {
		return `{"rules": [ {"id": "", "clauses": [` + clauseJSON + `]} ]}`
	}
	for _, cp := range makeClauseSerializationTestParams() {
		sp := segmentSerializationTestParams{
			name: "segment rule clause " + cp.name,
			segment: Segment{
				Rules: []SegmentRule{
					{
						Clauses: []Clause{cp.clause},
					},
				},
			},
			jsonString: makeSegmentJSONForClause(cp.jsonString),
		}
		for _, alt := range cp.jsonAltInputs {
			sp.jsonAltInputs = append(sp.jsonAltInputs, makeSegmentJSONForClause(alt))
		}
		ret = append(ret, sp)
	}

	return ret
}

func makeClauseSerializationTestParams() []clauseSerializationTestParams {
	return []clauseSerializationTestParams{
		{
			name: "simple",
			clause: Clause{
				Attribute: ldattr.NewLiteralRef("key"),
				Op:        OperatorIn,
				Values:    []ldvalue.Value{ldvalue.String("a")},
			},
			jsonString:    `{"attribute": "key", "op": "in", "values": ["a"], "negate": false}`,
			jsonAltInputs: []string{`{"attribute": "key", "op": "in", "values": ["a"]}`},
		},
		{
			name: "with kind",
			clause: Clause{
				ContextKind: ldcontext.Kind("org"),
				Attribute:   ldattr.NewLiteralRef("key"),
				Op:          OperatorIn,
				Values:      []ldvalue.Value{ldvalue.String("a")},
			},
			jsonString: `{"contextKind": "org", "attribute": "key", "op": "in", "values": ["a"], "negate": false}`,
		},
		{
			name: "with kind and complex attribute ref",
			clause: Clause{
				ContextKind: ldcontext.Kind("user"),
				Attribute:   ldattr.NewRef("/attr1/subprop"),
				Op:          OperatorIn,
				Values:      []ldvalue.Value{ldvalue.String("a")},
			},
			jsonString: `{"contextKind": "user", "attribute": "/attr1/subprop", "op": "in", "values": ["a"], "negate": false}`,
		},
		{
			name: "attribute is treated as plain name and not path when kind is omitted",
			clause: Clause{
				Attribute: ldattr.NewLiteralRef("/attr1/subprop"), // note NewLiteralRef, not NewRef
				Op:        OperatorIn,
				Values:    []ldvalue.Value{ldvalue.String("a")},
			},
			jsonString: `{"attribute": "/attr1/subprop", "op": "in", "values": ["a"], "negate": false}`,
		},
		{
			name: "invalid attribute ref",
			clause: Clause{
				ContextKind: ldcontext.Kind("user"),
				Attribute:   ldattr.NewRef("///"),
				Op:          OperatorIn,
				Values:      []ldvalue.Value{ldvalue.String("a")},
			},
			jsonString: `{"contextKind": "user", "attribute": "///", "op": "in", "values": ["a"], "negate": false}`,
		},
		{
			name: "with segmentMatch operator",
			clause: Clause{
				Op:     OperatorSegmentMatch,
				Values: []ldvalue.Value{ldvalue.String("a")},
			},
			jsonString: `{"attribute": "", "op": "segmentMatch", "values": ["a"], "negate": false}`,
			// note, attribute is serialized as "" in this case, not omitted
		},
		{
			name: "negated",
			clause: Clause{
				Attribute: ldattr.NewLiteralRef("key"),
				Op:        OperatorIn,
				Values:    []ldvalue.Value{ldvalue.String("a")},
				Negate:    true,
			},
			jsonString: `{"attribute": "key", "op": "in", "values": ["a"], "negate": true}`,
		},
	}
}

func makeRolloutSerializationTestParams() []rolloutSerializationTestParams {
	basicVariations := []WeightedVariation{{Variation: 1, Weight: 100000}}
	basicVariationsJSON := `[{"variation": 1, "weight": 100000}]`

	return []rolloutSerializationTestParams{
		{
			name:       "simple",
			rollout:    Rollout{Variations: basicVariations},
			jsonString: `{"variations": ` + basicVariationsJSON + `}`,
		},
		{
			name: "with context kind",
			rollout: Rollout{
				ContextKind: ldcontext.Kind("org"),
				Variations:  basicVariations,
			},
			jsonString: `{"contextKind": "org", "variations": ` + basicVariationsJSON + `}`,
		},
		{
			name: "with bucketBy",
			rollout: Rollout{
				BucketBy:   ldattr.NewLiteralRef("name"),
				Variations: basicVariations,
			},
			jsonString: `{"bucketBy": "name", "variations": ` + basicVariationsJSON + `}`,
		},
		{
			name: "with contextKind and complex bucketBy ref",
			rollout: Rollout{
				ContextKind: ldcontext.Kind("user"),
				BucketBy:    ldattr.NewRef("/attr1/subprop"),
				Variations:  basicVariations,
			},
			jsonString: `{"contextKind": "user", "bucketBy": "/attr1/subprop", "variations": ` + basicVariationsJSON + `}`,
		},
		{
			name: "bucketBy is treated as plain name and not path when kind is omitted",
			rollout: Rollout{
				BucketBy:   ldattr.NewLiteralRef("/attr1/subprop"), // note, NewLiteralRef rather than NewRef
				Variations: basicVariations,
			},
			jsonString: `{"bucketBy": "/attr1/subprop", "variations": ` + basicVariationsJSON + `}`,
		},
		{
			name: "invalid bucketBy ref",
			rollout: Rollout{
				ContextKind: ldcontext.Kind("user"),
				BucketBy:    ldattr.NewRef("///"),
				Variations:  basicVariations,
			},
			jsonString: `{"contextKind": "user", "bucketBy": "///", "variations": ` + basicVariationsJSON + `}`,
		},
		{
			name: "simple experiment",
			rollout: Rollout{
				Kind:       RolloutKindExperiment,
				Variations: basicVariations,
			},
			jsonString:    `{"kind": "experiment", "variations": ` + basicVariationsJSON + `}`,
			jsonAltInputs: []string{`{"kind": "experiment", "seed": null, "variations": ` + basicVariationsJSON + `}`},
		},
		{
			name: "experiment with seed",
			rollout: Rollout{
				Kind:       RolloutKindExperiment,
				Seed:       ldvalue.NewOptionalInt(12345),
				Variations: basicVariations,
			},
			jsonString: `{"kind": "experiment", "seed": 12345, "variations": ` + basicVariationsJSON + `}`,
		},
		{
			name: "experiment with untracked",
			rollout: Rollout{
				Kind: RolloutKindExperiment,
				Variations: []WeightedVariation{
					{Variation: 0, Weight: 75000},
					{Variation: 1, Weight: 25000, Untracked: true},
				},
			},
			jsonString: `{"kind": "experiment", "variations": [` +
				`{"variation": 0, "weight": 75000}, {"variation": 1, "weight": 25000, "untracked": true}]}`,
		},
	}
}

func mergeDefaultProperties(output json.RawMessage, defaults map[string]interface{}) json.RawMessage {
	var parsedOutput map[string]interface{}
	if err := json.Unmarshal(output, &parsedOutput); err != nil {
		return output
	}
	outMap := make(map[string]interface{})
	for k, v := range parsedOutput {
		outMap[k] = v
	}
	for k, v := range defaults {
		if _, found := outMap[k]; !found {
			outMap[k] = v
		}
	}
	data, err := json.Marshal(outMap)
	if err != nil {
		return output
	}
	return data
}
