package config

import (
	"bytes"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/influxdata/influxdb/v2"
	influxtesting "github.com/influxdata/influxdb/v2/testing"
)

func TestWriteConfigs(t *testing.T) {
	cases := []struct {
		name   string
		err    *influxdb.Error
		pp     Configs
		result string
	}{
		{
			name: "bad name -",
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  `"-" is not a valid config name`,
			},
		},
		{
			name: "bad name create",
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  `"create" is not a valid config name`,
			},
		},
		{
			name: "new config",
			pp: Configs{
				"default": Config{
					Token:  "token1",
					Org:    "org1",
					Host:   "http://localhost:9999",
					Active: true,
				},
			},
			result: `[default]
  url = "http://localhost:9999"
  token = "token1"
  org = "org1"
  active = true` + commentedStr,
		},
		{
			name: "multiple",
			pp: Configs{
				"config1": Config{
					Token: "token1",
					Host:  "host1",
				},
				"config2": Config{
					Token:  "token2",
					Host:   "host2",
					Org:    "org2",
					Active: true,
				},
				"config3": Config{
					Token: "token3",
					Host:  "host3",
					Org:   "org3",
				},
			},
			result: `[config1]
  url = "host1"
  token = "token1"
  org = ""

[config2]
  url = "host2"
  token = "token2"
  org = "org2"
  active = true

[config3]
  url = "host3"
  token = "token3"
  org = "org3"` + commentedStr,
		},
	}
	for _, c := range cases {
		var b1 bytes.Buffer
		err := (baseRW{w: &b1}).writeConfigs(c.pp)
		influxtesting.ErrorsEqual(t, err, err)
		if c.err == nil {
			if diff := cmp.Diff(c.result, b1.String()); diff != "" {
				t.Fatalf("write configs %s err, diff %s", c.name, diff)
			}
		}
	}
}

var commentedStr = `
# 
# [eu-central]
#   url = "https://eu-central-1-1.aws.cloud2.influxdata.com"
#   token = "XXX"
#   org = ""
# 
# [us-central]
#   url = "https://us-central1-1.gcp.cloud2.influxdata.com"
#   token = "XXX"
#   org = ""
# 
# [us-west]
#   url = "https://us-west-2-1.aws.cloud2.influxdata.com"
#   token = "XXX"
#   org = ""
`

func TestParseActiveConfig(t *testing.T) {
	cases := []struct {
		name   string
		hasErr bool
		src    string
		p      Config
	}{
		{
			name:   "bad src",
			src:    "bad [toml",
			hasErr: true,
		},
		{
			name:   "nothing",
			hasErr: true,
		},
		{
			name:   "conflicted",
			hasErr: true,
			src: `
			[a1]
			url = "host1"
			active =true
			[a2]
			url = "host2"
			active = true			
			`,
		},
		{
			name:   "one active",
			hasErr: false,
			src: `
			[a1]
			url = "host1"
			[a2]
			url = "host2"
			active = true
			[a3]
			url = "host3"
			[a4]
			url = "host4"				
			`,
			p: Config{
				Name:   "a2",
				Host:   "host2",
				Active: true,
			},
		},
	}
	for _, c := range cases {
		r := bytes.NewBufferString(c.src)
		p, err := ParseActiveConfig(r)
		if c.hasErr {
			if err == nil {
				t.Fatalf("parse active config %q failed, should have error, got nil", c.name)
			}
			continue
		}
		if diff := cmp.Diff(p, c.p); diff != "" {
			t.Fatalf("parse active config %s failed, diff %s", c.name, diff)
		}
	}
}

func TestParsePreviousActiveConfig(t *testing.T) {
	cases := []struct {
		name   string
		hasErr bool
		src    string
		p      Config
	}{
		{
			name:   "bad src",
			src:    "bad [toml",
			hasErr: true,
		},
		{
			name:   "nothing",
			hasErr: true,
		},
		{
			name:   "conflicted",
			hasErr: true,
			src: `
			[a1]
			url = "host1"
			previous =true
			[a2]
			url = "host2"
			previous = true			
			`,
		},
		{
			name:   "one previous active",
			hasErr: false,
			src: `
			[a1]
			url = "host1"
			[a2]
			url = "host2"
			previous = true
			[a3]
			url = "host3"
			[a4]
			url = "host4"				
			`,
			p: Config{
				Name:           "a2",
				Host:           "host2",
				PreviousActive: true,
			},
		},
	}
	for _, c := range cases {
		r := bytes.NewBufferString(c.src)
		p, err := (baseRW{r: r}).parsePreviousActive()
		if c.hasErr {
			if err == nil {
				t.Fatalf("parse previous active config %q failed, should have error, got nil", c.name)
			}
			continue
		}
		if diff := cmp.Diff(p, c.p); diff != "" {
			t.Fatalf("parse previous active config %s failed, diff %s", c.name, diff)
		}
	}
}

func TestConfigsSwith(t *testing.T) {
	cases := []struct {
		name   string
		old    Configs
		new    Configs
		target string
		err    error
	}{
		{
			name:   "not found",
			target: "p1",
			old: Configs{
				"a1": {Host: "host1"},
				"a2": {Host: "host2"},
			},
			new: Configs{
				"a1": {Host: "host1"},
				"a2": {Host: "host2"},
			},
			err: &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  `config "p1" is not found`,
			},
		},
		{
			name:   "regular switch",
			target: "a1",
			old: Configs{
				"a1": {Host: "host1"},
				"a2": {Host: "host2"},
				"a3": {Host: "host3", Active: true},
			},
			new: Configs{
				"a1": {Host: "host1", Active: true},
				"a2": {Host: "host2"},
				"a3": {Host: "host3", PreviousActive: true},
			},
			err: nil,
		},
		{
			name:   "regular to current active", // nothing should be changed
			target: "a3",
			old: Configs{
				"a1": {Host: "host1"},
				"a2": {Host: "host2"},
				"a3": {Host: "host3", Active: true},
			},
			new: Configs{
				"a1": {Host: "host1"},
				"a2": {Host: "host2"},
				"a3": {Host: "host3", Active: true},
			},
			err: nil,
		},
	}
	for _, c := range cases {
		err := c.old.Switch(c.target)
		influxtesting.ErrorsEqual(t, err, c.err)
		if diff := cmp.Diff(c.old, c.new); diff != "" {
			t.Fatalf("switch config %s failed, diff %s", c.name, diff)
		}
	}
}

func TestConfigCreate(t *testing.T) {
	cases := []struct {
		name   string
		exists Configs
		src    Config
		err    error
		result Config
		stored Configs
	}{
		{
			name: "invalid name",
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Msg:  "config name is empty",
			},
		},
		{
			name: "new",
			src: Config{
				Name:  "default",
				Host:  "host1",
				Org:   "org1",
				Token: "tok1",
			},
			result: Config{
				Name:  "default",
				Host:  "host1",
				Org:   "org1",
				Token: "tok1",
			},
			stored: Configs{
				"default": {
					Name:  "default",
					Host:  "host1",
					Org:   "org1",
					Token: "tok1",
				},
			},
		},
		{
			name: "new active",
			src: Config{
				Name:   "default",
				Host:   "host1",
				Org:    "org1",
				Token:  "tok1",
				Active: true,
			},
			result: Config{
				Name:   "default",
				Host:   "host1",
				Org:    "org1",
				Token:  "tok1",
				Active: true,
			},
			stored: Configs{
				"default": {
					Name:   "default",
					Host:   "host1",
					Org:    "org1",
					Token:  "tok1",
					Active: true,
				},
			},
		},
		{
			name: "conflict",
			exists: Configs{
				"default": {
					Name: "default",
					Host: "host1",
				},
			},
			src: Config{
				Name: "default",
				Host: "host1",
			},
			err: &influxdb.Error{
				Code: influxdb.EConflict,
				Msg:  `config "default" already exists`,
			},
		},
		{
			name: "existing",
			exists: Configs{
				"default": {
					Name:   "default",
					Host:   "host1",
					Org:    "org1",
					Token:  "tok1",
					Active: true,
				},
			},
			src: Config{
				Name:   "a1",
				Host:   "host1",
				Org:    "org1",
				Token:  "tok1",
				Active: true,
			},
			result: Config{
				Name:   "a1",
				Host:   "host1",
				Org:    "org1",
				Token:  "tok1",
				Active: true,
			},
			stored: Configs{
				"default": {
					Name:           "default",
					Host:           "host1",
					Org:            "org1",
					Token:          "tok1",
					PreviousActive: true,
				},
				"a1": {
					Name:   "a1",
					Host:   "host1",
					Org:    "org1",
					Token:  "tok1",
					Active: true,
				},
			},
		},
	}
	for _, c := range cases {
		svc, store := newBufferSVC()
		_ = store.writeConfigs(c.exists)
		result, err := svc.CreateConfig(c.src)
		influxtesting.ErrorsEqual(t, err, c.err)
		if err == nil {
			if diff := cmp.Diff(result, c.result); diff != "" {
				t.Fatalf("create config %s failed, diff %s", c.name, diff)
			}
			stored, err := store.ListConfigs()
			if err != nil {
				t.Fatalf("create config %s to list result, err %s", c.name, err.Error())
			}
			if diff := cmp.Diff(stored, c.stored); diff != "" {
				t.Fatalf("create config %s failed, diff %s", c.name, diff)
			}
		}
	}
}

func TestConfigSwitch(t *testing.T) {
	cases := []struct {
		name   string
		exists Configs
		src    string
		err    error
		result Config
		stored Configs
	}{
		{
			name: "empty",
			err: &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  `config "" is not found`,
			},
		},
		{
			name: "not found",
			src:  "default",
			err: &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  `config "default" is not found`,
			},
		},
		{
			name: "regular",
			exists: Configs{
				"a1": {
					Name:  "a1",
					Host:  "host1",
					Org:   "org1",
					Token: "tok1",
				},
				"a2": {
					Name:   "a2",
					Host:   "host2",
					Org:    "org2",
					Token:  "tok2",
					Active: true,
				},
			},
			src: "a1",
			result: Config{
				Name:   "a1",
				Host:   "host1",
				Org:    "org1",
				Token:  "tok1",
				Active: true,
			},
			stored: Configs{
				"a1": {
					Name:   "a1",
					Host:   "host1",
					Org:    "org1",
					Token:  "tok1",
					Active: true,
				},
				"a2": {
					Name:           "a2",
					Host:           "host2",
					Org:            "org2",
					Token:          "tok2",
					PreviousActive: true,
				},
			},
		},
		{
			name: "switch back",
			exists: Configs{
				"a1": {
					Name:           "a1",
					Host:           "host1",
					Org:            "org1",
					Token:          "tok1",
					PreviousActive: true,
				},
				"a2": {
					Name:   "a2",
					Host:   "host2",
					Org:    "org2",
					Token:  "tok2",
					Active: true,
				},
			},
			src: "-",
			result: Config{
				Name:   "a1",
				Host:   "host1",
				Org:    "org1",
				Token:  "tok1",
				Active: true,
			},
			stored: Configs{
				"a1": {
					Name:   "a1",
					Host:   "host1",
					Org:    "org1",
					Token:  "tok1",
					Active: true,
				},
				"a2": {
					Name:           "a2",
					Host:           "host2",
					Org:            "org2",
					Token:          "tok2",
					PreviousActive: true,
				},
			},
		},
		{
			name: "switch back with no previous",
			exists: Configs{
				"a1": {
					Name:  "a1",
					Host:  "host1",
					Org:   "org1",
					Token: "tok1",
				},
				"a2": {
					Name:   "a2",
					Host:   "host2",
					Org:    "org2",
					Token:  "tok2",
					Active: true,
				},
			},
			src: "-",
			err: &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  "previous activated config is not found",
			},
		},
	}
	for _, c := range cases {
		svc, store := newBufferSVC()
		_ = store.writeConfigs(c.exists)
		result, err := svc.SwitchActive(c.src)
		influxtesting.ErrorsEqual(t, err, c.err)
		if err == nil {
			if diff := cmp.Diff(result, c.result); diff != "" {
				t.Fatalf("switch config %s failed, diff %s", c.name, diff)
			}
			stored, err := store.ListConfigs()
			if err != nil {
				t.Fatalf("switch config %s to list result, err %s", c.name, err.Error())
			}
			if diff := cmp.Diff(stored, c.stored); diff != "" {
				t.Fatalf("switch config %s failed, diff %s", c.name, diff)
			}
		}
	}
}

func TestConfigUpdate(t *testing.T) {
	cases := []struct {
		name   string
		exists Configs
		src    Config
		err    error
		result Config
		stored Configs
	}{
		{
			name: "empty",
			err: &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  `config "" is not found`,
			},
		},
		{
			name: "not found",
			src: Config{
				Name:  "default",
				Host:  "host1",
				Org:   "org1",
				Token: "tok1",
			},
			err: &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  `config "default" is not found`,
			},
		},
		{
			name: "regular",
			exists: Configs{
				"a1": {
					Name:  "a1",
					Host:  "host1",
					Org:   "org1",
					Token: "tok1",
				},
				"a2": {
					Name:   "a2",
					Host:   "host2",
					Org:    "org2",
					Token:  "tok2",
					Active: true,
				},
			},
			src: Config{
				Name:   "a1",
				Host:   "host11",
				Org:    "org11",
				Token:  "tok11",
				Active: true,
			},
			result: Config{
				Name:   "a1",
				Host:   "host11",
				Org:    "org11",
				Token:  "tok11",
				Active: true,
			},
			stored: Configs{
				"a1": {
					Name:   "a1",
					Host:   "host11",
					Org:    "org11",
					Token:  "tok11",
					Active: true,
				},
				"a2": {
					Name:           "a2",
					Host:           "host2",
					Org:            "org2",
					Token:          "tok2",
					PreviousActive: true,
				},
			},
		},
	}
	for _, c := range cases {
		svc, store := newBufferSVC()
		_ = store.writeConfigs(c.exists)
		result, err := svc.UpdateConfig(c.src)
		influxtesting.ErrorsEqual(t, err, c.err)
		if err == nil {
			if diff := cmp.Diff(result, c.result); diff != "" {
				t.Fatalf("update config %s failed, diff %s", c.name, diff)
			}
			stored, err := store.ListConfigs()
			if err != nil {
				t.Fatalf("update config %s to list result, err %s", c.name, err.Error())
			}
			if diff := cmp.Diff(stored, c.stored); diff != "" {
				t.Fatalf("update config %s failed, diff %s", c.name, diff)
			}
		}
	}
}

func TestConfigDelete(t *testing.T) {
	cases := []struct {
		name   string
		exists Configs
		target string
		err    error
		result Config
		stored Configs
	}{
		{
			name: "empty",
			err: &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  `config "" is not found`,
			},
		},
		{
			name:   "not found",
			target: "bad",
			exists: Configs{
				"default": {
					Name: "default",
					Host: "host1",
				},
			},
			err: &influxdb.Error{
				Code: influxdb.ENotFound,
				Msg:  `config "bad" is not found`,
			},
		},
		{
			name: "regular",
			exists: Configs{
				"default": {
					Name: "default",
					Host: "host1",
				},
			},
			target: "default",
			result: Config{
				Name: "default",
				Host: "host1",
			},
			stored: Configs{},
		},
		{
			name: "more than 1",
			exists: Configs{
				"a1": {
					Host:   "host1",
					Org:    "org1",
					Token:  "tok1",
					Active: true,
				},
				"a2": {
					Host:  "host2",
					Org:   "org2",
					Token: "tok2",
				},
			},
			target: "a1",
			result: Config{
				Name:   "a1",
				Host:   "host1",
				Org:    "org1",
				Token:  "tok1",
				Active: true,
			},
			stored: Configs{
				"a2": {
					Name:  "a2",
					Host:  "host2",
					Org:   "org2",
					Token: "tok2",
				},
			},
		},
	}
	for _, c := range cases {
		svc, store := newBufferSVC()
		_ = store.writeConfigs(c.exists)
		result, err := svc.DeleteConfig(c.target)
		influxtesting.ErrorsEqual(t, err, c.err)
		if err == nil {
			if diff := cmp.Diff(result, c.result); diff != "" {
				t.Fatalf("delete config %s failed, diff %s", c.name, diff)
			}
			stored, err := store.ListConfigs()
			if err != nil {
				t.Fatalf("delete config %s to list result, err %s", c.name, err.Error())
			}
			if diff := cmp.Diff(stored, c.stored); diff != "" {
				t.Fatalf("delete config %s failed, diff %s", c.name, diff)
			}
		}
	}
}

func newBufferSVC() (ConfigsService, *bytesStore) {
	store := new(bytesStore)
	return newConfigsSVC(store), store
}

type bytesStore struct {
	data []byte
}

func (s *bytesStore) writeConfigs(cfgs Configs) error {
	var b bytes.Buffer
	if err := (baseRW{w: &b}).writeConfigs(cfgs); err != nil {
		return err
	}
	s.data = b.Bytes()
	return nil
}

func (s *bytesStore) ListConfigs() (Configs, error) {
	return baseRW{
		r: bytes.NewBuffer(s.data),
	}.ListConfigs()
}

func (s *bytesStore) parsePreviousActive() (Config, error) {
	return (baseRW{
		r: bytes.NewBuffer(s.data),
	}).parsePreviousActive()
}
