package main

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/invopop/jsonschema"
	orderedmap "github.com/pb33f/ordered-map/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestCopyAliasFieldComments verifies that field comments from source types are correctly copied to alias types.
// This is important for type aliases like `type RpmArchive RpmDBEntry` where the alias should inherit all field descriptions.
func TestCopyAliasFieldComments(t *testing.T) {
	tests := []struct {
		name         string
		commentMap   map[string]string
		aliases      map[string]string
		wantComments map[string]string
	}{
		{
			name: "copies field comments from source type to alias",
			commentMap: map[string]string{
				"github.com/anchore/syft/syft/pkg.RpmDBEntry":       "RpmDBEntry represents all captured data from a RPM DB package entry.",
				"github.com/anchore/syft/syft/pkg.RpmDBEntry.Name":  "Name is the RPM package name.",
				"github.com/anchore/syft/syft/pkg.RpmDBEntry.Epoch": "Epoch is the version epoch.",
			},
			aliases: map[string]string{
				"RpmArchive": "RpmDBEntry",
			},
			wantComments: map[string]string{
				"github.com/anchore/syft/syft/pkg.RpmDBEntry":       "RpmDBEntry represents all captured data from a RPM DB package entry.",
				"github.com/anchore/syft/syft/pkg.RpmDBEntry.Name":  "Name is the RPM package name.",
				"github.com/anchore/syft/syft/pkg.RpmDBEntry.Epoch": "Epoch is the version epoch.",
				"github.com/anchore/syft/syft/pkg.RpmArchive.Name":  "Name is the RPM package name.",
				"github.com/anchore/syft/syft/pkg.RpmArchive.Epoch": "Epoch is the version epoch.",
			},
		},
		{
			name: "handles multiple aliases",
			commentMap: map[string]string{
				"github.com/anchore/syft/syft/pkg.DpkgDBEntry":              "DpkgDBEntry represents data from dpkg.",
				"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Package":      "Package is the package name.",
				"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Architecture": "Architecture is the target arch.",
			},
			aliases: map[string]string{
				"DpkgArchiveEntry": "DpkgDBEntry",
				"DpkgSnapshot":     "DpkgDBEntry",
			},
			wantComments: map[string]string{
				"github.com/anchore/syft/syft/pkg.DpkgDBEntry":                   "DpkgDBEntry represents data from dpkg.",
				"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Package":           "Package is the package name.",
				"github.com/anchore/syft/syft/pkg.DpkgDBEntry.Architecture":      "Architecture is the target arch.",
				"github.com/anchore/syft/syft/pkg.DpkgArchiveEntry.Package":      "Package is the package name.",
				"github.com/anchore/syft/syft/pkg.DpkgArchiveEntry.Architecture": "Architecture is the target arch.",
				"github.com/anchore/syft/syft/pkg.DpkgSnapshot.Package":          "Package is the package name.",
				"github.com/anchore/syft/syft/pkg.DpkgSnapshot.Architecture":     "Architecture is the target arch.",
			},
		},
		{
			name: "does not copy non-field comments",
			commentMap: map[string]string{
				"github.com/anchore/syft/syft/pkg.SomeType":       "SomeType struct comment.",
				"github.com/anchore/syft/syft/pkg.SomeType.Field": "Field comment.",
			},
			aliases: map[string]string{
				"AliasType": "SomeType",
			},
			wantComments: map[string]string{
				"github.com/anchore/syft/syft/pkg.SomeType":        "SomeType struct comment.",
				"github.com/anchore/syft/syft/pkg.SomeType.Field":  "Field comment.",
				"github.com/anchore/syft/syft/pkg.AliasType.Field": "Field comment.",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// create temp dir for testing
			tmpDir := t.TempDir()

			// create a test go file with type aliases
			testFile := filepath.Join(tmpDir, "test.go")
			content := "package test\n\n"
			for alias, source := range tt.aliases {
				content += "type " + alias + " " + source + "\n"
			}
			err := os.WriteFile(testFile, []byte(content), 0644)
			require.NoError(t, err)

			// make a copy of the comment map since the function modifies it
			commentMap := make(map[string]string)
			for k, v := range tt.commentMap {
				commentMap[k] = v
			}

			// run the function
			copyAliasFieldComments(commentMap, tmpDir)

			// verify results
			assert.Equal(t, tt.wantComments, commentMap)
		})
	}
}

func TestFindTypeAliases(t *testing.T) {
	tests := []struct {
		name        string
		fileContent string
		wantAliases map[string]string
	}{
		{
			name: "finds simple type alias",
			fileContent: `package test

type RpmArchive RpmDBEntry
type DpkgArchiveEntry DpkgDBEntry
`,
			wantAliases: map[string]string{
				"RpmArchive":       "RpmDBEntry",
				"DpkgArchiveEntry": "DpkgDBEntry",
			},
		},
		{
			name: "ignores struct definitions",
			fileContent: `package test

type MyStruct struct {
	Field string
}

type AliasType BaseType
`,
			wantAliases: map[string]string{
				"AliasType": "BaseType",
			},
		},
		{
			name: "ignores interface definitions",
			fileContent: `package test

type MyInterface interface {
	Method()
}

type AliasType BaseType
`,
			wantAliases: map[string]string{
				"AliasType": "BaseType",
			},
		},
		{
			name: "handles multiple files",
			fileContent: `package test

type Alias1 Base1
type Alias2 Base2
`,
			wantAliases: map[string]string{
				"Alias1": "Base1",
				"Alias2": "Base2",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// create temp dir
			tmpDir := t.TempDir()

			// write test file
			testFile := filepath.Join(tmpDir, "test.go")
			err := os.WriteFile(testFile, []byte(tt.fileContent), 0644)
			require.NoError(t, err)

			// run function
			aliases := findTypeAliases(tmpDir)

			// verify
			assert.Equal(t, tt.wantAliases, aliases)
		})
	}
}

func TestHasDescriptionInAlternatives(t *testing.T) {
	tests := []struct {
		name   string
		schema *jsonschema.Schema
		want   bool
	}{
		{
			name: "returns true when oneOf has description",
			schema: &jsonschema.Schema{
				OneOf: []*jsonschema.Schema{
					{Description: "First alternative"},
					{Type: "null"},
				},
			},
			want: true,
		},
		{
			name: "returns true when anyOf has description",
			schema: &jsonschema.Schema{
				AnyOf: []*jsonschema.Schema{
					{Description: "First alternative"},
					{Type: "null"},
				},
			},
			want: true,
		},
		{
			name: "returns false when no alternatives have descriptions",
			schema: &jsonschema.Schema{
				OneOf: []*jsonschema.Schema{
					{Type: "integer"},
					{Type: "null"},
				},
			},
			want: false,
		},
		{
			name: "returns false when no oneOf or anyOf",
			schema: &jsonschema.Schema{
				Type: "string",
			},
			want: false,
		},
		{
			name: "returns true when any alternative in oneOf has description",
			schema: &jsonschema.Schema{
				OneOf: []*jsonschema.Schema{
					{Type: "integer"},
					{Type: "string", Description: "Second alternative"},
					{Type: "null"},
				},
			},
			want: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := hasDescriptionInAlternatives(tt.schema)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestWarnMissingDescriptions(t *testing.T) {
	tests := []struct {
		name              string
		schema            *jsonschema.Schema
		metadataNames     []string
		wantTypeWarnings  int
		wantFieldWarnings int
	}{
		{
			name: "no warnings when all types have descriptions",
			schema: &jsonschema.Schema{
				Definitions: map[string]*jsonschema.Schema{
					"TypeA": {
						Description: "Type A description",
						Properties: newOrderedMap(map[string]*jsonschema.Schema{
							"field1": {Type: "string", Description: "Field 1"},
						}),
					},
				},
			},
			metadataNames:     []string{"TypeA"},
			wantTypeWarnings:  0,
			wantFieldWarnings: 0,
		},
		{
			name: "warns about missing type description",
			schema: &jsonschema.Schema{
				Definitions: map[string]*jsonschema.Schema{
					"TypeA": {
						Properties: newOrderedMap(map[string]*jsonschema.Schema{
							"field1": {Type: "string", Description: "Field 1"},
						}),
					},
				},
			},
			metadataNames:     []string{"TypeA"},
			wantTypeWarnings:  1,
			wantFieldWarnings: 0,
		},
		{
			name: "warns about missing field description",
			schema: &jsonschema.Schema{
				Definitions: map[string]*jsonschema.Schema{
					"TypeA": {
						Description: "Type A description",
						Properties: newOrderedMap(map[string]*jsonschema.Schema{
							"field1": {Type: "string"},
						}),
					},
				},
			},
			metadataNames:     []string{"TypeA"},
			wantTypeWarnings:  0,
			wantFieldWarnings: 1,
		},
		{
			name: "skips fields with references",
			schema: &jsonschema.Schema{
				Definitions: map[string]*jsonschema.Schema{
					"TypeA": {
						Description: "Type A description",
						Properties: newOrderedMap(map[string]*jsonschema.Schema{
							"field1": {Ref: "#/$defs/OtherType"},
						}),
					},
				},
			},
			metadataNames:     []string{"TypeA"},
			wantTypeWarnings:  0,
			wantFieldWarnings: 0,
		},
		{
			name: "skips fields with items that are references",
			schema: &jsonschema.Schema{
				Definitions: map[string]*jsonschema.Schema{
					"TypeA": {
						Description: "Type A description",
						Properties: newOrderedMap(map[string]*jsonschema.Schema{
							"field1": {
								Type:  "array",
								Items: &jsonschema.Schema{Ref: "#/$defs/OtherType"},
							},
						}),
					},
				},
			},
			metadataNames:     []string{"TypeA"},
			wantTypeWarnings:  0,
			wantFieldWarnings: 0,
		},
		{
			name: "skips fields with oneOf containing descriptions",
			schema: &jsonschema.Schema{
				Definitions: map[string]*jsonschema.Schema{
					"TypeA": {
						Description: "Type A description",
						Properties: newOrderedMap(map[string]*jsonschema.Schema{
							"field1": {
								OneOf: []*jsonschema.Schema{
									{Type: "integer", Description: "Integer value"},
									{Type: "null"},
								},
							},
						}),
					},
				},
			},
			metadataNames:     []string{"TypeA"},
			wantTypeWarnings:  0,
			wantFieldWarnings: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// capture stderr output would require more complex testing
			// for now, just verify the function runs without panicking
			require.NotPanics(t, func() {
				warnMissingDescriptions(tt.schema, tt.metadataNames)
			})
		})
	}
}

// helper to create an ordered map from a regular map
func newOrderedMap(m map[string]*jsonschema.Schema) *orderedmap.OrderedMap[string, *jsonschema.Schema] {
	om := orderedmap.New[string, *jsonschema.Schema]()
	for k, v := range m {
		om.Set(k, v)
	}
	return om
}
