package deepmerge_test

import (
	"encoding/json"
	"errors"
	"testing"

	"github.com/TwiN/deepmerge"
)

func TestJSON(t *testing.T) {
	scenarios := []struct {
		name        string
		config      deepmerge.Config
		dst         string
		src         string
		expected    string
		expectedErr error
	}{
		{
			name:        "invalid-dst",
			dst:         ``,
			src:         `{}`,
			expected:    `{}`,
			expectedErr: errors.New("unexpected end of JSON input"),
		},
		{
			name:        "invalid-src",
			dst:         `{}`,
			src:         ``,
			expected:    `{}`,
			expectedErr: errors.New("unexpected end of JSON input"),
		},
		{
			name: "simple-endpoint-merge",
			dst: `{
  "endpoints": [
    {
      "name": "one",
      "url": "https://example.com",
      "client": {
        "timeout": "5s"
      },
      "conditions": [
        "[CONNECTED] == true",
        "[STATUS] == 200"
      ],
      "alerts": [
        {
          "type": "slack",
          "failure-threshold": 5
        }
      ]
    },
    {
      "name": "two",
      "url": "https://example.org",
      "conditions": [
        "len([BODY]) > 0"
      ]
    }
  ]
}`,
			src: `{
  "endpoints": [
    {
      "name": "three",
      "url": "https://twin.sh/health",
      "conditions": [
        "[STATUS] == 200",
        "[BODY].status == UP"
      ]
    }
  ]
}`,
			expected: `{
  "endpoints": [
    {
      "name": "one",
      "url": "https://example.com",
      "client": {
        "timeout": "5s"
      },
      "conditions": [
        "[CONNECTED] == true",
        "[STATUS] == 200"
      ],
      "alerts": [
        {
          "type": "slack",
          "failure-threshold": 5
        }
      ]
    },
    {
      "name": "two",
      "url": "https://example.org",
      "conditions": [
        "len([BODY]) > 0"
      ]
    },
    {
      "name": "three",
      "url": "https://twin.sh/health",
      "conditions": [
        "[STATUS] == 200",
        "[BODY].status == UP"
      ]
    }
  ]
}`,
		},
		{
			name: "deep-merge-with-map-slice-and-primitive",
			dst: `{
  "metrics": true,
  "alerting": {
    "slack": {
      "webhook-url": "https://hooks.slack.com/services/xxx/yyy/zzz",
      "default-alert": {
        "description": "health check failed",
        "send-on-resolved": true,
        "failure-threshold": 5,
        "success-threshold": 5
      }
    }
  },
  "endpoints": [
    {
      "name": "example",
      "url": "https://example.org",
      "interval": "5s"
    }
  ]
}`,
			src: `{
  "debug": true,
  "alerting": {
    "discord": {
      "webhook-url": "https://discord.com/api/webhooks/xxx/yyy"
    }
  },
  "endpoints": [
    {
      "name": "frontend",
      "url": "https://example.com"
    }
  ]
}`,
			expected: `{
  "metrics": true,
  "debug": true,
  "alerting": {
    "discord": {
      "webhook-url": "https://discord.com/api/webhooks/xxx/yyy"
    },
    "slack": {
      "webhook-url": "https://hooks.slack.com/services/xxx/yyy/zzz",
      "default-alert": {
        "description": "health check failed",
        "send-on-resolved": true,
        "failure-threshold": 5,
        "success-threshold": 5
      }
    }
  },
  "endpoints": [
    {
      "interval": "5s",
      "name": "example",
      "url": "https://example.org"
    },
    {
      "name": "frontend",
      "url": "https://example.com"
    }
  ]
}`,
		},
		{ // only maps and slices can be merged. If there are duplicate keys that have a primitive value, then that's an error.
			name:   "duplicate-key-with-primitive-value",
			config: deepmerge.Config{PreventMultipleDefinitionsOfKeysWithPrimitiveValue: true}, // NOTE: true is the default
			dst:    `{"metrics": true}`,
			src:    `{"metrics": false}`,

			expectedErr: deepmerge.ErrKeyWithPrimitiveValueDefinedMoreThanOnce,
		},
		{
			name:     "duplicate-key-with-primitive-value-with-PreventMultipleDefinitionsOfKeysWithPrimitiveValue-set-to-false",
			config:   deepmerge.Config{PreventMultipleDefinitionsOfKeysWithPrimitiveValue: false},
			dst:      `{"metrics": true, "debug": true}`,
			src:      `{"metrics": false}`,
			expected: `{"metrics": false, "debug": true}`,
		},
		{
			name: "readme-example",
			dst: `{
  "debug": true,
  "client": {
    "insecure": true
  },
  "users": [
    {
      "id": 1,
      "firstName": "John",
      "lastName": "Doe"
    },
    {
      "id": 2,
      "firstName": "Jane",
      "lastName": "Doe"
    }
  ]
}`,
			src: `{
  "client": {
    "timeout": "5s"
  },
  "users": [
    {
      "id": 3,
      "firstName": "Bob",
      "lastName": "Smith"
    }
  ]
}`,
			expected: `{
  "client": {
    "insecure": true,
    "timeout": "5s"
  },
  "debug": true,
  "users": [
    {
      "firstName": "John",
      "id": 1,
      "lastName": "Doe"
    },
    {
      "firstName": "Jane",
      "id": 2,
      "lastName": "Doe"
    },
    {
      "firstName": "Bob",
      "id": 3,
      "lastName": "Smith"
    }
  ]
}`,
		},
	}
	for _, scenario := range scenarios {
		t.Run(scenario.name, func(t *testing.T) {
			output, err := deepmerge.JSON([]byte(scenario.dst), []byte(scenario.src), scenario.config)
			if !errors.Is(err, scenario.expectedErr) && !(scenario.expectedErr != nil && err.Error() == scenario.expectedErr.Error()) {
				t.Errorf("[%s] expected error %v, got %v", scenario.name, scenario.expectedErr, err)
			}
			// Just so we don't have to worry about the formatting, we'll unmarshal the output and marshal it again.
			expectedAsMap, outputAsMap := make(map[string]interface{}), make(map[string]interface{})
			if len(output) > 0 {
				if err := json.Unmarshal(output, &outputAsMap); err != nil {
					t.Errorf("[%s] failed to unmarshal output: %v", scenario.name, err)
				}
			}
			if len(scenario.expected) > 0 {
				if err := json.Unmarshal([]byte(scenario.expected), &expectedAsMap); err != nil {
					t.Errorf("[%s] failed to unmarshal expected: %v", scenario.name, err)
				}
			}
			formattedOutput, err := json.Marshal(outputAsMap)
			if err != nil {
				t.Errorf("[%s] should've been able to re-marshal output: %v", scenario.name, err)
			}
			formattedExpected, err := json.Marshal(expectedAsMap)
			if err != nil {
				t.Errorf("[%s] should've been able to re-marshal expected: %v", scenario.name, err)
			}
			// Compare what we got vs what we expected
			if string(formattedOutput) != string(formattedExpected) {
				t.Errorf("[%s] expected:\n%s\n\ngot:\n%s", scenario.name, string(formattedExpected), string(formattedOutput))
			}
		})
	}
}
