// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package renderer

import (
	"context"
	"encoding/json"
	"strings"
	"testing"

	"github.com/pb33f/libopenapi/datamodel/high/base"
	"github.com/pb33f/libopenapi/datamodel/low"
	lowbase "github.com/pb33f/libopenapi/datamodel/low/base"
	"github.com/pb33f/libopenapi/orderedmap"
	"github.com/pb33f/libopenapi/utils"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.yaml.in/yaml/v4"
)

type fakeMockable struct {
	Schema   *base.SchemaProxy
	Example  any
	Examples *orderedmap.Map[string, *base.Example]
}

type fakeMockableButWithASchemaNotAProxy struct {
	Schema   *base.Schema
	Example  any
	Examples *orderedmap.Map[string, *base.Example]
}

var simpleFakeMockSchema = `type: string
enum: [magic-herbs]`

var objectFakeMockSchema = `type: object
properties:
  coffee:
    type: string
    minLength: 6
  herbs:
    type: number
    minimum: 350
    maximum: 400`

func createFakeMock(mock string, values map[string]any, example any) *fakeMockable {
	var root yaml.Node
	_ = yaml.Unmarshal([]byte(mock), &root)
	var lowProxy lowbase.SchemaProxy
	_ = lowProxy.Build(context.Background(), &root, root.Content[0], nil)
	lowRef := low.NodeReference[*lowbase.SchemaProxy]{
		Value: &lowProxy,
	}
	highSchema := base.NewSchemaProxy(&lowRef)
	examples := orderedmap.New[string, *base.Example]()

	for k, v := range values {
		examples.Set(k, &base.Example{
			Value: utils.CreateYamlNode(v),
		})
	}
	return &fakeMockable{
		Schema:   highSchema,
		Example:  example,
		Examples: examples,
	}
}

func createFakeMockWithoutProxy(mock string, values map[string]any, example any) *fakeMockableButWithASchemaNotAProxy {
	var root yaml.Node
	_ = yaml.Unmarshal([]byte(mock), &root)
	var lowProxy lowbase.SchemaProxy
	_ = lowProxy.Build(context.Background(), &root, root.Content[0], nil)
	lowRef := low.NodeReference[*lowbase.SchemaProxy]{
		Value: &lowProxy,
	}
	highSchema := base.NewSchemaProxy(&lowRef)
	examples := orderedmap.New[string, *base.Example]()

	for k, v := range values {
		examples.Set(k, &base.Example{
			Value: utils.CreateYamlNode(v),
		})
	}
	return &fakeMockableButWithASchemaNotAProxy{
		Schema:   highSchema.Schema(),
		Example:  example,
		Examples: examples,
	}
}

func TestNewMockGenerator(t *testing.T) {
	mg := NewMockGenerator(JSON)
	assert.NotNil(t, mg)
}

func TestNewMockGeneratorWithDictionary(t *testing.T) {
	mg := NewMockGeneratorWithDictionary("", JSON)
	assert.NotNil(t, mg)
}

func TestMockGenerator_GenerateJSONMock_NoObject(t *testing.T) {
	mg := NewMockGenerator(JSON)

	var isNil any

	mock, err := mg.GenerateMock(isNil, "")
	assert.NoError(t, err)
	assert.Nil(t, mock)
}

func TestMockGenerator_GenerateJSONMock_BadObject(t *testing.T) {
	type NotMockable struct{}

	mg := NewMockGenerator(JSON)
	mock, err := mg.GenerateMock(&NotMockable{}, "")
	assert.Error(t, err)
	assert.Nil(t, mock)
}

func TestMockGenerator_GenerateJSONMock_EmptyObject(t *testing.T) {
	mg := NewMockGenerator(JSON)
	mock, err := mg.GenerateMock(&fakeMockable{}, "")
	assert.NoError(t, err)
	assert.Nil(t, mock)
}

func TestMockGenerator_GenerateJSONMock_SuppliedExample_JSON(t *testing.T) {
	fakeExample := map[string]any{
		"fish-and-chips": "cod-and-chips-twice",
	}
	fake := createFakeMock(simpleFakeMockSchema, nil, fakeExample)
	mg := NewMockGenerator(JSON)
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)
	assert.Equal(t, "{\"fish-and-chips\":\"cod-and-chips-twice\"}", string(mock))
}

func TestMockGenerator_GenerateJSONMock_SuppliedExample_YAML(t *testing.T) {
	fakeExample := map[string]any{
		"fish-and-chips": "cod-and-chips-twice",
	}
	fake := createFakeMock(simpleFakeMockSchema, nil, fakeExample)
	mg := NewMockGenerator(YAML)
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)
	assert.Equal(t, "fish-and-chips: cod-and-chips-twice", strings.TrimSpace(string(mock)))
}

func TestMockGenerator_GenerateJSONMock_MultiExamples_NoName_JSON(t *testing.T) {
	fakeExample := map[string]any{
		"exampleOne": map[string]any{
			"fish-and-chips": "cod-and-chips-twice",
		},
		"exampleTwo": map[string]any{
			"rice-and-peas": "brown-or-white-rice",
		},
	}
	fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil)
	mg := NewMockGenerator(JSON)
	mock, err := mg.GenerateMock(fake, "JimmyJammyJimJams") // does not exist
	assert.NoError(t, err)
	assert.NotEmpty(t, string(mock))
}

func TestMockGenerator_GenerateJSONMock_MultiExamples_JSON(t *testing.T) {
	fakeExample := map[string]any{
		"exampleOne": map[string]any{
			"fish-and-chips": "cod-and-chips-twice",
		},
		"exampleTwo": map[string]any{
			"rice-and-peas": "brown-or-white-rice",
		},
	}
	fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil)
	mg := NewMockGenerator(JSON)
	mock, err := mg.GenerateMock(fake, "exampleTwo")
	assert.NoError(t, err)
	assert.Equal(t, "{\"rice-and-peas\":\"brown-or-white-rice\"}", string(mock))
}

func TestMockGenerator_GenerateJSONMock_MultiExamples_PrettyJSON(t *testing.T) {
	fakeExample := map[string]any{
		"exampleOne": map[string]any{
			"fish-and-chips": "cod-and-chips-twice",
		},
		"exampleTwo": map[string]any{
			"rice-and-peas": "brown-or-white-rice",
			"peas":          "buttery",
		},
	}
	fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil)
	mg := NewMockGenerator(JSON)
	mg.SetPretty()
	mock, err := mg.GenerateMock(fake, "exampleTwo")
	assert.NoError(t, err)
	assert.Equal(t, "{\n  \"peas\": \"buttery\",\n  \"rice-and-peas\": \"brown-or-white-rice\"\n}", string(mock))
}

func TestMockGenerator_GenerateJSONMock_MultiExamples_YAML(t *testing.T) {
	fakeExample := map[string]any{
		"exampleOne": map[string]any{
			"fish-and-chips": "cod-and-chips-twice",
		},
		"exampleTwo": map[string]any{
			"rice-and-peas": "brown-or-white-rice",
		},
	}
	fake := createFakeMock(simpleFakeMockSchema, fakeExample, nil)
	mg := NewMockGenerator(YAML)
	mock, err := mg.GenerateMock(fake, "exampleTwo")
	assert.NoError(t, err)
	assert.Equal(t, "rice-and-peas: brown-or-white-rice", strings.TrimSpace(string(mock)))
}

func TestMockGenerator_GenerateJSONMock_NoExamples_JSON(t *testing.T) {
	fake := createFakeMock(simpleFakeMockSchema, nil, nil)
	mg := NewMockGenerator(JSON)
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)
	assert.Equal(t, `"magic-herbs"`, string(mock))
}

func TestMockGenerator_GenerateJSONMock_NoExamples_YAML(t *testing.T) {
	fake := createFakeMock(simpleFakeMockSchema, nil, nil)
	mg := NewMockGenerator(YAML)
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)
	assert.Equal(t, "magic-herbs", strings.TrimSpace(string(mock)))
}

func TestMockGenerator_GenerateJSONMock_Object_NoExamples_JSON(t *testing.T) {
	fake := createFakeMock(objectFakeMockSchema, nil, nil)
	mg := NewMockGenerator(JSON)
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)

	// re-serialize back into a map and check the values
	var m map[string]any
	err = json.Unmarshal(mock, &m)
	assert.NoError(t, err)

	assert.Len(t, m, 2)
	assert.GreaterOrEqual(t, len(m["coffee"].(string)), 6)
	assert.GreaterOrEqual(t, m["herbs"].(float64), float64(350))
	assert.LessOrEqual(t, m["herbs"].(float64), float64(400))
}

func TestMockGenerator_GenerateJSONMock_Object_NoExamples_YAML(t *testing.T) {
	fake := createFakeMock(objectFakeMockSchema, nil, nil)
	mg := NewMockGenerator(YAML)
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)

	// re-serialize back into a map and check the values
	var m map[string]any
	err = yaml.Unmarshal(mock, &m)
	assert.NoError(t, err)

	assert.Len(t, m, 2)
	assert.GreaterOrEqual(t, len(m["coffee"].(string)), 6)
	assert.GreaterOrEqual(t, m["herbs"].(int), 350)
	assert.LessOrEqual(t, m["herbs"].(int), 400)
}

// should result in the exact same output as the above test
func TestMockGenerator_GenerateJSONMock_Object_RawSchema(t *testing.T) {
	fake := createFakeMockWithoutProxy(objectFakeMockSchema, nil, nil)

	mg := NewMockGenerator(YAML)
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)

	// re-serialize back into a map and check the values
	var m map[string]any
	err = yaml.Unmarshal(mock, &m)
	assert.NoError(t, err)

	assert.Len(t, m, 2)
	assert.GreaterOrEqual(t, len(m["coffee"].(string)), 6)
	assert.GreaterOrEqual(t, m["herbs"].(int), 350)
	assert.LessOrEqual(t, m["herbs"].(int), 400)
}

func TestMockGenerator_GenerateMock_YamlNode(t *testing.T) {
	mg := NewMockGenerator(YAML)

	type mockable struct {
		Example  *yaml.Node
		Examples *orderedmap.Map[string, *base.Example]
	}

	mock, err := mg.GenerateMock(&mockable{
		Example: utils.CreateStringNode("hello"),
	}, "")
	assert.NoError(t, err)
	assert.Equal(t, "hello", strings.TrimSpace(string(mock)))
}

func TestMockGenerator_GenerateMock_YamlNode_Nil(t *testing.T) {
	mg := NewMockGenerator(YAML)

	var example *yaml.Node

	type mockable struct {
		Example  any
		Examples *orderedmap.Map[string, *base.Example]
	}

	examples := orderedmap.New[string, *base.Example]()
	examples.Set("exampleOne", &base.Example{
		Value: utils.CreateStringNode("hello"),
	})

	mock, err := mg.GenerateMock(&mockable{
		Example:  example,
		Examples: examples,
	}, "")
	assert.NoError(t, err)
	assert.Equal(t, "hello", strings.TrimSpace(string(mock)))
}

func TestMockGenerator_GenerateJSONMock_Object_SchemaExamples(t *testing.T) {
	yml := `type: object
examples:
  - name: happy days
    description: a terrible show from a time that never existed.
  - name: robocop
    description: perhaps the best cyberpunk movie ever made.
properties:
  name:
    type: string
    example: nameExample
  description:
    type: string
    example: descriptionExample`

	fake := createFakeMock(yml, nil, nil)
	mg := NewMockGenerator(YAML)
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)

	// re-serialize back into a map and check the values
	var m map[string]any
	err = yaml.Unmarshal(mock, &m)
	assert.NoError(t, err)

	assert.Len(t, m, 2)
	assert.Equal(t, "happy days", m["name"].(string))
	assert.Equal(t, "a terrible show from a time that never existed.", m["description"].(string))
}

func TestMockGenerator_GenerateJSONMock_DirectSchema_SchemaExamples(t *testing.T) {
	yml := `type: object
examples:
  - name: happy days
    description: a terrible show from a time that never existed.
  - name: robocop
    description: perhaps the best cyberpunk movie ever made.
properties:
  name:
    type: string
    example: nameExample
  description:
    type: string
    example: descriptionExample`

	fake := createFakeMockWithoutProxy(yml, nil, nil)
	mg := NewMockGenerator(YAML)
	mock, err := mg.GenerateMock(fake.Schema, "")
	assert.NoError(t, err)

	var m map[string]any
	err = yaml.Unmarshal(mock, &m)
	assert.NoError(t, err)

	assert.Len(t, m, 2)
	assert.Equal(t, "happy days", m["name"].(string))
	assert.Equal(t, "a terrible show from a time that never existed.", m["description"].(string))
}

func TestMockGenerator_GenerateJSONMock_Object_SchemaExamples_Preferred(t *testing.T) {
	yml := `type: object
examples:
  - name: happy days
    description: a terrible show from a time that never existed.
  - name: robocop
    description: perhaps the best cyberpunk movie ever made.
properties:
  name:
    type: string
    example: nameExample
  description:
    type: string
    example: descriptionExample`

	fake := createFakeMock(yml, nil, nil)
	mg := NewMockGenerator(YAML)
	mock, err := mg.GenerateMock(fake, "1")
	assert.NoError(t, err)

	// re-serialize back into a map and check the values
	var m map[string]any
	err = yaml.Unmarshal(mock, &m)
	assert.NoError(t, err)

	assert.Len(t, m, 2)
	assert.Equal(t, "robocop", m["name"].(string))
	assert.Equal(t, "perhaps the best cyberpunk movie ever made.", m["description"].(string))
}

func TestMockGenerator_GenerateJSONMock_Object_SchemaExample(t *testing.T) {
	yml := `type: object
example:
  name: robocop
  description: perhaps the best cyberpunk movie ever made.
properties:
  name:
    type: string
    example: nameExample
  description:
    type: string
    example: descriptionExample`

	fake := createFakeMock(yml, nil, nil)
	mg := NewMockGenerator(YAML)
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)

	// re-serialize back into a map and check the values
	var m map[string]any
	err = yaml.Unmarshal(mock, &m)
	assert.NoError(t, err)

	assert.Len(t, m, 2)
	assert.Equal(t, "robocop", m["name"].(string))
	assert.Equal(t, "perhaps the best cyberpunk movie ever made.", m["description"].(string))
}

func TestMockGenerator_EmptyMock(t *testing.T) {
	mg := NewMockGenerator(YAML)

	sp := &lowbase.SchemaProxy{}
	sp.Build(context.Background(), &yaml.Node{}, &yaml.Node{}, nil)

	mock, err := mg.GenerateMock(&fakeMockable{
		Schema:   base.NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{Value: sp}),
		Example:  nil,
		Examples: nil,
	}, "")
	assert.Error(t, err)
	assert.Nil(t, mock)
}

func TestMockGenerator_GeneratePropertyExamples(t *testing.T) {
	fake := createFakeMock(`type: object
required:
  - id
  - name
properties:
  id:
    type: integer
    example: 123
  name:
    type: string
    example: "John Doe"
  active:
    type: boolean
    example: true
  balance:
    type: number
    format: float
    example: 99.99
  tags:
    type: array
    items:
      type: string
    example: ["tag1", "tag2", "tag3"]
`, nil, nil)

	for name, tc := range map[string]struct {
		mockGen      func() *MockGenerator
		expectedMock string
	}{
		"OnlyRequired": {
			mockGen: func() *MockGenerator {
				mg := NewMockGenerator(JSON)
				return mg
			},
			expectedMock: `{"id":123,"name":"John Doe"}`,
		},
		"All": {
			mockGen: func() *MockGenerator {
				mg := NewMockGenerator(JSON)

				// Test schema rendering for property examples, regardless of
				// whether the property is marked as required or not.
				mg.DisableRequiredCheck()

				return mg
			},
			expectedMock: `{"active":true,"balance":99.99,"id":123,"name":"John Doe","tags":["tag1","tag2","tag3"]}`,
		},
	} {
		tc := tc
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			mock, err := tc.mockGen().GenerateMock(fake, "")
			require.NoError(t, err)

			assert.Equal(t, tc.expectedMock, string(mock))
		})
	}
}

func TestMockGenerator_SetSeed_Deterministic(t *testing.T) {
	objectSchema := `type: object
properties:
  name:
    type: string
    minLength: 5
    maxLength: 10
  age:
    type: integer
    minimum: 18
    maximum: 99`

	fake := createFakeMock(objectSchema, nil, nil)

	// Generate two mocks with the same seed
	mg1 := NewMockGenerator(JSON)
	mg1.SetSeed(42)
	mock1, err := mg1.GenerateMock(fake, "")
	assert.NoError(t, err)

	mg2 := NewMockGenerator(JSON)
	mg2.SetSeed(42)
	mock2, err := mg2.GenerateMock(fake, "")
	assert.NoError(t, err)

	// They should be identical
	assert.Equal(t, string(mock1), string(mock2))

	// Generate a third mock with a different seed
	mg3 := NewMockGenerator(JSON)
	mg3.SetSeed(123)
	mock3, err := mg3.GenerateMock(fake, "")
	assert.NoError(t, err)

	// It should be different from the first two
	assert.NotEqual(t, string(mock1), string(mock3))
}

func TestMockGenerator_GenerateMock_PickNamedExample(t *testing.T) {
	mg := NewMockGenerator(YAML)

	type mockable struct {
		Example  *base.Example
		Examples *orderedmap.Map[string, *base.Example]
	}

	inlineExample := &base.Example{
		Value: utils.CreateStringNode("inline example"),
	}
	examples := orderedmap.New[string, *base.Example]()
	examples.Set("exampleOne", &base.Example{
		Value: utils.CreateStringNode("example 1"),
	})

	mock, err := mg.GenerateMock(&mockable{
		Example:  inlineExample,
		Examples: examples,
	}, "exampleOne")
	assert.NoError(t, err)
	assert.Equal(t, "example 1", strings.TrimSpace(string(mock)))
}

func TestMockGenerator_GenerateMock_PickOldestExample(t *testing.T) {
	mg := NewMockGenerator(YAML)

	type mockable struct {
		Example  any
		Examples *orderedmap.Map[string, *base.Example]
	}

	examples := orderedmap.New[string, *base.Example]()
	examples.Set("exampleOne", &base.Example{
		Value: utils.CreateStringNode("example 1"),
	})
	examples.Set("exampleTwo", &base.Example{
		Value: utils.CreateStringNode("example 2"),
	})

	mock, err := mg.GenerateMock(&mockable{
		Examples: examples,
	}, "nonexisting example")
	assert.NoError(t, err)
	assert.Equal(t, "example 1", strings.TrimSpace(string(mock)))
}

func TestMockGenerator_GenerateMock_GetInline(t *testing.T) {
	mg := NewMockGenerator(YAML)

	type mockable struct {
		Example  any
		Examples *orderedmap.Map[string, *base.Example]
	}
	inlineExample := &yaml.Node{
		Kind:  yaml.ScalarNode,
		Value: "inline example",
	}
	examples := orderedmap.New[string, *base.Example]()
	examples.Set("exampleOne", &base.Example{
		Value: utils.CreateStringNode("example 1"),
	})
	examples.Set("exampleTwo", &base.Example{
		Value: utils.CreateStringNode("example 2"),
	})

	mock, err := mg.GenerateMock(&mockable{
		Example:  inlineExample,
		Examples: examples,
	}, "nonexisting example")
	assert.NoError(t, err)
	assert.Equal(t, "inline example", strings.TrimSpace(string(mock)))
}

func TestMockGenerator_UnresolvedRefProperty(t *testing.T) {
	// Construct a schema programmatically with one inline and one unresolved ref property
	props := orderedmap.New[string, *base.SchemaProxy]()
	props.Set("name", base.CreateSchemaProxy(&base.Schema{
		Type:        []string{"string"},
		ParentProxy: base.CreateSchemaProxy(&base.Schema{}),
	}))
	props.Set("broken", base.CreateSchemaProxyRef("#/components/schemas/Missing"))

	schema := &base.Schema{
		Type:        []string{"object"},
		Properties:  props,
		ParentProxy: base.CreateSchemaProxy(&base.Schema{}),
	}
	schemaProxy := base.CreateSchemaProxy(schema)

	fake := &fakeMockable{
		Schema:   schemaProxy,
		Example:  nil,
		Examples: nil,
	}
	mg := NewMockGenerator(JSON)
	mg.DisableRequiredCheck()
	mock, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)

	var m map[string]any
	err = json.Unmarshal(mock, &m)
	assert.NoError(t, err)
	assert.IsType(t, "", m["name"])
	// "broken" should be null in JSON output
	val, exists := m["broken"]
	assert.True(t, exists)
	assert.Nil(t, val)
}

func TestMockGenerator_SetUnresolvedRefHandler(t *testing.T) {
	props := orderedmap.New[string, *base.SchemaProxy]()
	props.Set("broken", base.CreateSchemaProxyRef("#/components/schemas/Missing"))

	schema := &base.Schema{
		Type:        []string{"object"},
		Properties:  props,
		ParentProxy: base.CreateSchemaProxy(&base.Schema{}),
	}
	schemaProxy := base.CreateSchemaProxy(schema)

	fake := &fakeMockable{
		Schema:   schemaProxy,
		Example:  nil,
		Examples: nil,
	}
	mg := NewMockGenerator(JSON)
	mg.DisableRequiredCheck()

	var callbackName string
	mg.SetUnresolvedRefHandler(func(name string, proxy *base.SchemaProxy, err error) {
		callbackName = name
	})

	_, err := mg.GenerateMock(fake, "")
	assert.NoError(t, err)
	assert.Equal(t, "broken", callbackName)
}

func TestMockGenerator_TopLevelUnresolvedRef(t *testing.T) {
	mg := NewMockGenerator(JSON)
	fake := &fakeMockable{
		Schema:   base.CreateSchemaProxyRef("#/components/schemas/Missing"),
		Example:  nil,
		Examples: nil,
	}
	mock, err := mg.GenerateMock(fake, "")
	assert.Error(t, err)
	assert.Nil(t, mock)
	assert.Contains(t, err.Error(), "#/components/schemas/Missing")
	assert.Contains(t, err.Error(), "unable to resolve schema reference")
}

func TestMockGenerator_TopLevelBuildError(t *testing.T) {
	// Create a low-level SchemaProxy with a nil value node. When Schema() is called,
	// the low-level Schema.Build receives nil root and returns "cannot build schema from a nil node".
	// This sets buildError on the proxy while IsReference() remains false.
	sp := &lowbase.SchemaProxy{}
	_ = sp.Build(context.Background(), nil, nil, nil)

	highProxy := base.NewSchemaProxy(&low.NodeReference[*lowbase.SchemaProxy]{
		Value: sp,
	})

	// Verify preconditions
	assert.Nil(t, highProxy.Schema())
	assert.False(t, highProxy.IsReference())
	assert.NotNil(t, highProxy.GetBuildError())

	mg := NewMockGenerator(JSON)
	fake := &fakeMockable{
		Schema:   highProxy,
		Example:  nil,
		Examples: nil,
	}
	mock, err := mg.GenerateMock(fake, "")
	assert.Error(t, err)
	assert.Nil(t, mock)
	assert.Contains(t, err.Error(), "unable to build schema for mock generation")
}
