package syftjson

import (
	"encoding/json"
	"os"
	"path/filepath"
	"slices"
	"testing"

	"github.com/iancoleman/strcase"
	"github.com/stretchr/testify/require"

	"github.com/anchore/syft/internal/packagemetadata"
)

type schema struct {
	Schema string                `json:"$schema"`
	ID     string                `json:"$id"`
	Ref    string                `json:"$ref"`
	Defs   map[string]properties `json:"$defs"`
}

type properties struct {
	Properties map[string]any `json:"properties"`
	Type       string         `json:"type"`
	Required   []string       `json:"required"`
}

func (p properties) fields() []string {
	var result []string

	for k := range p.Properties {
		result = append(result, k)
	}
	return result
}

func Test_JSONSchemaConventions(t *testing.T) {
	// read schema/json/schema-latest.json
	// look at all attributes and ensure that all fields are camelCase
	// we want to strictly follow https://google.github.io/styleguide/javaguide.html#s5.3-camel-case
	//
	// > Convert the phrase to plain ASCII and remove any apostrophes. For example, "Müller's algorithm" might become "Muellers algorithm".
	// > Divide this result into words, splitting on spaces and any remaining punctuation (typically hyphens).
	// > Recommended: if any word already has a conventional camel-case appearance in common usage, split this into its constituent parts (e.g., "AdWords" becomes "ad words"). Note that a word such as "iOS" is not really in camel case per se; it defies any convention, so this recommendation does not apply.
	// > Now lowercase everything (including acronyms), then uppercase only the first character of:
	// > ... each word, to yield upper camel case, or
	// > ... each word except the first, to yield lower camel case
	// > Finally, join all the words into a single identifier.
	//
	// This means that acronyms should be treated as words (e.g. "HttpServer" not "HTTPServer")

	root, err := packagemetadata.RepoRoot()
	require.NoError(t, err)

	contents, err := os.ReadFile(filepath.Join(root, "schema", "json", "schema-latest.json"))
	require.NoError(t, err)

	var s schema
	require.NoError(t, json.Unmarshal(contents, &s))

	require.NotEmpty(t, s.Defs)

	for name, def := range s.Defs {
		checkAndConvertFields(t, name, def.fields())
	}
}

func checkAndConvertFields(t *testing.T, path string, properties []string) {
	for _, fieldName := range properties {
		if pass, exp := isFollowingConvention(path, fieldName); !pass {
			t.Logf("%s: has non camel case field: %q (expected %q)", path, fieldName, exp)
		}

	}
}

func isFollowingConvention(path, fieldName string) (bool, string) {
	exp := strcase.ToLowerCamel(fieldName)
	result := exp == fieldName

	exception := func(exceptions ...string) (bool, string) {
		if slices.Contains(exceptions, fieldName) {
			return true, fieldName
		}
		return result, exp
	}

	// add exceptions as needed... these are grandfathered in and will be addressed in a future breaking schema change
	// ideally in the future there will be no exceptions to the camel case convention for fields
	switch path {
	case "Coordinates", "Location":
		return exception("layerID")
	case "MicrosoftKbPatch":
		return exception("product_id")
	case "HaskellHackageStackLockEntry":
		return exception("snapshotURL")
	case "LinuxRelease":
		return exception("imageID", "supportURL", "privacyPolicyURL", "versionID", "variantID", "homeURL", "buildID", "bugReportURL")
	case "CConanLockV2Entry":
		return exception("packageID")
	case "CConanInfoEntry":
		return exception("package_id")
	case "PhpComposerInstalledEntry", "PhpComposerLockEntry":
		return exception("notification-url", "require-dev")
	case "LinuxKernelArchive":
		return exception("rwRootFS")
	case "CConanLockEntry":
		return exception("build_requires", "py_requires", "package_id")
	case "FileMetadataEntry":
		return exception("userID", "groupID")
	case "DartPubspecLockEntry":
		return exception("hosted_url", "vcs_url")
	case "ELFSecurityFeatures":
		return exception("relRO")
	}
	return result, exp
}
