package config

import (
	"encoding/json"
	"fmt"
	"testing"

	"github.com/safing/portmaster/base/log"
)

func parseAndReplaceConfig(jsonData string) error {
	m, err := JSONToMap([]byte(jsonData))
	if err != nil {
		return err
	}

	validationErrors, _ := ReplaceConfig(m)
	if len(validationErrors) > 0 {
		return fmt.Errorf("%d errors, first: %w", len(validationErrors), validationErrors[0])
	}
	return nil
}

func parseAndReplaceDefaultConfig(jsonData string) error {
	m, err := JSONToMap([]byte(jsonData))
	if err != nil {
		return err
	}

	validationErrors, _ := ReplaceDefaultConfig(m)
	if len(validationErrors) > 0 {
		return fmt.Errorf("%d errors, first: %w", len(validationErrors), validationErrors[0])
	}
	return nil
}

func quickRegister(t *testing.T, key string, optType OptionType, defaultValue interface{}) {
	t.Helper()

	err := Register(&Option{
		Name:           key,
		Key:            key,
		Description:    "test config",
		ReleaseLevel:   ReleaseLevelStable,
		ExpertiseLevel: ExpertiseLevelUser,
		OptType:        optType,
		DefaultValue:   defaultValue,
	})
	if err != nil {
		t.Fatal(err)
	}
}

func TestGet(t *testing.T) { //nolint:paralleltest
	// reset
	options = make(map[string]*Option)

	err := log.Start()
	if err != nil {
		t.Fatal(err)
	}

	quickRegister(t, "monkey", OptTypeString, "c")
	quickRegister(t, "zebras/zebra", OptTypeStringArray, []string{"a", "b"})
	quickRegister(t, "elephant", OptTypeInt, -1)
	quickRegister(t, "hot", OptTypeBool, false)
	quickRegister(t, "cold", OptTypeBool, true)

	err = parseAndReplaceConfig(`
	{
		"monkey": "a",
		"zebras": {
			"zebra": ["black", "white"]
		},
		"elephant": 2,
		"hot": true,
		"cold": false
	}
	`)
	if err != nil {
		t.Fatal(err)
	}

	err = parseAndReplaceDefaultConfig(`
	{
		"monkey": "b",
		"snake": "0",
		"elephant": 0
	}
	`)
	if err != nil {
		t.Fatal(err)
	}

	monkey := GetAsString("monkey", "none")
	if monkey() != "a" {
		t.Errorf("monkey should be a, is %s", monkey())
	}

	zebra := GetAsStringArray("zebras/zebra", []string{})
	if len(zebra()) != 2 || zebra()[0] != "black" || zebra()[1] != "white" {
		t.Errorf("zebra should be [\"black\", \"white\"], is %v", zebra())
	}

	elephant := GetAsInt("elephant", -1)
	if elephant() != 2 {
		t.Errorf("elephant should be 2, is %d", elephant())
	}

	hot := GetAsBool("hot", false)
	if !hot() {
		t.Errorf("hot should be true, is %v", hot())
	}

	cold := GetAsBool("cold", true)
	if cold() {
		t.Errorf("cold should be false, is %v", cold())
	}

	err = parseAndReplaceConfig(`
	{
		"monkey": "3"
	}
	`)
	if err != nil {
		t.Fatal(err)
	}

	if monkey() != "3" {
		t.Errorf("monkey should be 0, is %s", monkey())
	}

	if elephant() != 0 {
		t.Errorf("elephant should be 0, is %d", elephant())
	}

	zebra()
	hot()

	// concurrent
	GetAsString("monkey", "none")()
	GetAsStringArray("zebras/zebra", []string{})()
	GetAsInt("elephant", -1)()
	GetAsBool("hot", false)()

	// perspective

	// load data
	pLoaded := make(map[string]interface{})
	err = json.Unmarshal([]byte(`{
		"monkey": "a",
		"zebras": {
			"zebra": ["black", "white"]
		},
		"elephant": 2,
		"hot": true,
		"cold": false
	}`), &pLoaded)
	if err != nil {
		t.Fatal(err)
	}

	// create
	p, err := NewPerspective(pLoaded)
	if err != nil {
		t.Fatal(err)
	}

	monkeyVal, ok := p.GetAsString("monkey")
	if !ok || monkeyVal != "a" {
		t.Errorf("[perspective] monkey should be a, is %+v", monkeyVal)
	}

	zebraVal, ok := p.GetAsStringArray("zebras/zebra")
	if !ok || len(zebraVal) != 2 || zebraVal[0] != "black" || zebraVal[1] != "white" {
		t.Errorf("[perspective] zebra should be [\"black\", \"white\"], is %+v", zebraVal)
	}

	elephantVal, ok := p.GetAsInt("elephant")
	if !ok || elephantVal != 2 {
		t.Errorf("[perspective] elephant should be 2, is %+v", elephantVal)
	}

	hotVal, ok := p.GetAsBool("hot")
	if !ok || !hotVal {
		t.Errorf("[perspective] hot should be true, is %+v", hotVal)
	}

	coldVal, ok := p.GetAsBool("cold")
	if !ok || coldVal {
		t.Errorf("[perspective] cold should be false, is %+v", coldVal)
	}
}

func TestReleaseLevel(t *testing.T) { //nolint:paralleltest
	// reset
	options = make(map[string]*Option)
	registerReleaseLevelOption()

	// setup
	subsystemOption := &Option{
		Name:           "test subsystem",
		Key:            "subsystem/test",
		Description:    "test config",
		ReleaseLevel:   ReleaseLevelStable,
		ExpertiseLevel: ExpertiseLevelUser,
		OptType:        OptTypeBool,
		DefaultValue:   false,
	}
	err := Register(subsystemOption)
	if err != nil {
		t.Fatal(err)
	}
	err = SetConfigOption("subsystem/test", true)
	if err != nil {
		t.Fatal(err)
	}
	testSubsystem := GetAsBool("subsystem/test", false)

	// test option level stable
	subsystemOption.ReleaseLevel = ReleaseLevelStable
	err = SetConfigOption(releaseLevelKey, ReleaseLevelNameStable)
	if err != nil {
		t.Fatal(err)
	}
	if !testSubsystem() {
		t.Error("should be active")
	}
	err = SetConfigOption(releaseLevelKey, ReleaseLevelNameBeta)
	if err != nil {
		t.Fatal(err)
	}
	if !testSubsystem() {
		t.Error("should be active")
	}
	err = SetConfigOption(releaseLevelKey, ReleaseLevelNameExperimental)
	if err != nil {
		t.Fatal(err)
	}
	if !testSubsystem() {
		t.Error("should be active")
	}

	// test option level beta
	subsystemOption.ReleaseLevel = ReleaseLevelBeta
	err = SetConfigOption(releaseLevelKey, ReleaseLevelNameStable)
	if err != nil {
		t.Fatal(err)
	}
	if testSubsystem() {
		t.Errorf("should be inactive: opt=%d system=%d", subsystemOption.ReleaseLevel, getReleaseLevel())
	}
	err = SetConfigOption(releaseLevelKey, ReleaseLevelNameBeta)
	if err != nil {
		t.Fatal(err)
	}
	if !testSubsystem() {
		t.Error("should be active")
	}
	err = SetConfigOption(releaseLevelKey, ReleaseLevelNameExperimental)
	if err != nil {
		t.Fatal(err)
	}
	if !testSubsystem() {
		t.Error("should be active")
	}

	// test option level experimental
	subsystemOption.ReleaseLevel = ReleaseLevelExperimental
	err = SetConfigOption(releaseLevelKey, ReleaseLevelNameStable)
	if err != nil {
		t.Fatal(err)
	}
	if testSubsystem() {
		t.Error("should be inactive")
	}
	err = SetConfigOption(releaseLevelKey, ReleaseLevelNameBeta)
	if err != nil {
		t.Fatal(err)
	}
	if testSubsystem() {
		t.Error("should be inactive")
	}
	err = SetConfigOption(releaseLevelKey, ReleaseLevelNameExperimental)
	if err != nil {
		t.Fatal(err)
	}
	if !testSubsystem() {
		t.Error("should be active")
	}
}

func BenchmarkGetAsStringCached(b *testing.B) {
	// reset
	options = make(map[string]*Option)

	// Setup
	err := parseAndReplaceConfig(`{
		"monkey": "banana"
	}`)
	if err != nil {
		b.Fatal(err)
	}
	monkey := GetAsString("monkey", "no banana")

	// Reset timer for precise results
	b.ResetTimer()

	// Start benchmark
	for range b.N {
		monkey()
	}
}

func BenchmarkGetAsStringRefetch(b *testing.B) {
	// Setup
	err := parseAndReplaceConfig(`{
		"monkey": "banana"
	}`)
	if err != nil {
		b.Fatal(err)
	}

	// Reset timer for precise results
	b.ResetTimer()

	// Start benchmark
	for range b.N {
		getValueCache("monkey", nil, OptTypeString)
	}
}

func BenchmarkGetAsIntCached(b *testing.B) {
	// Setup
	err := parseAndReplaceConfig(`{
		"elephant": 1
	}`)
	if err != nil {
		b.Fatal(err)
	}
	elephant := GetAsInt("elephant", -1)

	// Reset timer for precise results
	b.ResetTimer()

	// Start benchmark
	for range b.N {
		elephant()
	}
}

func BenchmarkGetAsIntRefetch(b *testing.B) {
	// Setup
	err := parseAndReplaceConfig(`{
		"elephant": 1
	}`)
	if err != nil {
		b.Fatal(err)
	}

	// Reset timer for precise results
	b.ResetTimer()

	// Start benchmark
	for range b.N {
		getValueCache("elephant", nil, OptTypeInt)
	}
}