// Copyright 2023 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package config

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	v1 "github.com/fatedier/frp/pkg/config/v1"
)

const tomlServerContent = `
bindAddr = "127.0.0.1"
kcpBindPort = 7000
quicBindPort = 7001
tcpmuxHTTPConnectPort = 7005
custom404Page = "/abc.html"
transport.tcpKeepalive = 10
`

const yamlServerContent = `
bindAddr: 127.0.0.1
kcpBindPort: 7000
quicBindPort: 7001
tcpmuxHTTPConnectPort: 7005
custom404Page: /abc.html
transport:
  tcpKeepalive: 10
`

const jsonServerContent = `
{
  "bindAddr": "127.0.0.1",
  "kcpBindPort": 7000,
  "quicBindPort": 7001,
  "tcpmuxHTTPConnectPort": 7005,
  "custom404Page": "/abc.html",
  "transport": {
    "tcpKeepalive": 10
  }
}
`

func TestLoadServerConfig(t *testing.T) {
	tests := []struct {
		name    string
		content string
	}{
		{"toml", tomlServerContent},
		{"yaml", yamlServerContent},
		{"json", jsonServerContent},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			require := require.New(t)
			svrCfg := v1.ServerConfig{}
			err := LoadConfigure([]byte(test.content), &svrCfg, true)
			require.NoError(err)
			require.EqualValues("127.0.0.1", svrCfg.BindAddr)
			require.EqualValues(7000, svrCfg.KCPBindPort)
			require.EqualValues(7001, svrCfg.QUICBindPort)
			require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort)
			require.EqualValues("/abc.html", svrCfg.Custom404Page)
			require.EqualValues(10, svrCfg.Transport.TCPKeepAlive)
		})
	}
}

// Test that loading in strict mode fails when the config is invalid.
func TestLoadServerConfigStrictMode(t *testing.T) {
	tests := []struct {
		name    string
		content string
	}{
		{"toml", tomlServerContent},
		{"yaml", yamlServerContent},
		{"json", jsonServerContent},
	}

	for _, strict := range []bool{false, true} {
		for _, test := range tests {
			t.Run(fmt.Sprintf("%s-strict-%t", test.name, strict), func(t *testing.T) {
				require := require.New(t)
				// Break the content with an innocent typo
				brokenContent := strings.Replace(test.content, "bindAddr", "bindAdur", 1)
				svrCfg := v1.ServerConfig{}
				err := LoadConfigure([]byte(brokenContent), &svrCfg, strict)
				if strict {
					require.ErrorContains(err, "bindAdur")
				} else {
					require.NoError(err)
					// BindAddr didn't get parsed because of the typo.
					require.EqualValues("", svrCfg.BindAddr)
				}
			})
		}
	}
}

func TestRenderWithTemplate(t *testing.T) {
	tests := []struct {
		name    string
		content string
		want    string
	}{
		{"toml", tomlServerContent, tomlServerContent},
		{"yaml", yamlServerContent, yamlServerContent},
		{"json", jsonServerContent, jsonServerContent},
		{"template numeric", `key = {{ 123 }}`, "key = 123"},
		{"template string", `key = {{ "xyz" }}`, "key = xyz"},
		{"template quote", `key = {{ printf "%q" "with space" }}`, `key = "with space"`},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			require := require.New(t)
			got, err := RenderWithTemplate([]byte(test.content), nil)
			require.NoError(err)
			require.EqualValues(test.want, string(got))
		})
	}
}

func TestCustomStructStrictMode(t *testing.T) {
	require := require.New(t)

	proxyStr := `
serverPort = 7000

[[proxies]]
name = "test"
type = "tcp"
remotePort = 6000
`
	clientCfg := v1.ClientConfig{}
	err := LoadConfigure([]byte(proxyStr), &clientCfg, true)
	require.NoError(err)

	proxyStr += `unknown = "unknown"`
	err = LoadConfigure([]byte(proxyStr), &clientCfg, true)
	require.Error(err)

	visitorStr := `
serverPort = 7000

[[visitors]]
name = "test"
type = "stcp"
bindPort = 6000
serverName = "server"
`
	err = LoadConfigure([]byte(visitorStr), &clientCfg, true)
	require.NoError(err)

	visitorStr += `unknown = "unknown"`
	err = LoadConfigure([]byte(visitorStr), &clientCfg, true)
	require.Error(err)

	pluginStr := `
serverPort = 7000

[[proxies]]
name = "test"
type = "tcp"
remotePort = 6000
[proxies.plugin]
type = "unix_domain_socket"
unixPath = "/tmp/uds.sock"
`
	err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
	require.NoError(err)
	pluginStr += `unknown = "unknown"`
	err = LoadConfigure([]byte(pluginStr), &clientCfg, true)
	require.Error(err)
}

func TestLoadClientConfigStrictMode_UnknownPluginField(t *testing.T) {
	require := require.New(t)

	content := `
serverPort = 7000

[[proxies]]
name = "test"
type = "tcp"
localPort = 6000
[proxies.plugin]
type = "http2https"
localAddr = "127.0.0.1:8080"
unknownInPlugin = "value"
`

	clientCfg := v1.ClientConfig{}

	err := LoadConfigure([]byte(content), &clientCfg, false)
	require.NoError(err)

	err = LoadConfigure([]byte(content), &clientCfg, true)
	require.ErrorContains(err, "unknownInPlugin")
}

// TestYAMLMergeInStrictMode tests that YAML merge functionality works
// even in strict mode by properly handling dot-prefixed fields
func TestYAMLMergeInStrictMode(t *testing.T) {
	require := require.New(t)

	yamlContent := `
serverAddr: "127.0.0.1"
serverPort: 7000

.common: &common
  type: stcp
  secretKey: "test-secret"
  localIP: 127.0.0.1
  transport:
    useEncryption: true
    useCompression: true

proxies:
- name: ssh
  localPort: 22
  <<: *common
- name: web
  localPort: 80
  <<: *common
`

	clientCfg := v1.ClientConfig{}
	// This should work in strict mode
	err := LoadConfigure([]byte(yamlContent), &clientCfg, true)
	require.NoError(err)

	// Verify the merge worked correctly
	require.Equal("127.0.0.1", clientCfg.ServerAddr)
	require.Equal(7000, clientCfg.ServerPort)
	require.Len(clientCfg.Proxies, 2)

	// Check first proxy
	sshProxy := clientCfg.Proxies[0].ProxyConfigurer
	require.Equal("ssh", sshProxy.GetBaseConfig().Name)
	require.Equal("stcp", sshProxy.GetBaseConfig().Type)

	// Check second proxy
	webProxy := clientCfg.Proxies[1].ProxyConfigurer
	require.Equal("web", webProxy.GetBaseConfig().Name)
	require.Equal("stcp", webProxy.GetBaseConfig().Type)
}

// TestOptimizedYAMLProcessing tests the optimization logic for YAML processing
func TestOptimizedYAMLProcessing(t *testing.T) {
	require := require.New(t)

	yamlWithDotFields := []byte(`
serverAddr: "127.0.0.1"
.common: &common
  type: stcp
proxies:
- name: test
  <<: *common
`)

	yamlWithoutDotFields := []byte(`
serverAddr: "127.0.0.1"
proxies:
- name: test
  type: tcp
  localPort: 22
`)

	// Test that YAML without dot fields works in strict mode
	clientCfg := v1.ClientConfig{}
	err := LoadConfigure(yamlWithoutDotFields, &clientCfg, true)
	require.NoError(err)
	require.Equal("127.0.0.1", clientCfg.ServerAddr)
	require.Len(clientCfg.Proxies, 1)
	require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)

	// Test that YAML with dot fields still works in strict mode
	err = LoadConfigure(yamlWithDotFields, &clientCfg, true)
	require.NoError(err)
	require.Equal("127.0.0.1", clientCfg.ServerAddr)
	require.Len(clientCfg.Proxies, 1)
	require.Equal("test", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Name)
	require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
}

func TestFilterClientConfigurers_PreserveRawNamesAndNoMutation(t *testing.T) {
	require := require.New(t)

	enabled := true
	proxyCfg := &v1.TCPProxyConfig{}
	proxyCfg.Name = "proxy-raw"
	proxyCfg.Type = "tcp"
	proxyCfg.LocalPort = 10080
	proxyCfg.Enabled = &enabled

	visitorCfg := &v1.XTCPVisitorConfig{}
	visitorCfg.Name = "visitor-raw"
	visitorCfg.Type = "xtcp"
	visitorCfg.ServerName = "server-raw"
	visitorCfg.FallbackTo = "fallback-raw"
	visitorCfg.SecretKey = "secret"
	visitorCfg.BindPort = 10081
	visitorCfg.Enabled = &enabled

	common := &v1.ClientCommonConfig{
		User: "alice",
	}

	proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
	require.Len(proxies, 1)
	require.Len(visitors, 1)

	p := proxies[0].GetBaseConfig()
	require.Equal("proxy-raw", p.Name)
	require.Empty(p.LocalIP)

	v := visitors[0].GetBaseConfig()
	require.Equal("visitor-raw", v.Name)
	require.Equal("server-raw", v.ServerName)
	require.Empty(v.BindAddr)

	xtcp := visitors[0].(*v1.XTCPVisitorConfig)
	require.Equal("fallback-raw", xtcp.FallbackTo)
	require.Empty(xtcp.Protocol)
}

func TestCompleteProxyConfigurers_PreserveRawNames(t *testing.T) {
	require := require.New(t)

	enabled := true
	proxyCfg := &v1.TCPProxyConfig{}
	proxyCfg.Name = "proxy-raw"
	proxyCfg.Type = "tcp"
	proxyCfg.LocalPort = 10080
	proxyCfg.Enabled = &enabled

	proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg})
	require.Len(proxies, 1)

	p := proxies[0].GetBaseConfig()
	require.Equal("proxy-raw", p.Name)
	require.Equal("127.0.0.1", p.LocalIP)
}

func TestCompleteVisitorConfigurers_PreserveRawNames(t *testing.T) {
	require := require.New(t)

	enabled := true
	visitorCfg := &v1.XTCPVisitorConfig{}
	visitorCfg.Name = "visitor-raw"
	visitorCfg.Type = "xtcp"
	visitorCfg.ServerName = "server-raw"
	visitorCfg.FallbackTo = "fallback-raw"
	visitorCfg.SecretKey = "secret"
	visitorCfg.BindPort = 10081
	visitorCfg.Enabled = &enabled

	visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg})
	require.Len(visitors, 1)

	v := visitors[0].GetBaseConfig()
	require.Equal("visitor-raw", v.Name)
	require.Equal("server-raw", v.ServerName)
	require.Equal("127.0.0.1", v.BindAddr)

	xtcp := visitors[0].(*v1.XTCPVisitorConfig)
	require.Equal("fallback-raw", xtcp.FallbackTo)
	require.Equal("quic", xtcp.Protocol)
}

func TestCompleteProxyConfigurers_Idempotent(t *testing.T) {
	require := require.New(t)

	proxyCfg := &v1.TCPProxyConfig{}
	proxyCfg.Name = "proxy"
	proxyCfg.Type = "tcp"
	proxyCfg.LocalPort = 10080

	proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg})
	firstProxyJSON, err := json.Marshal(proxies[0])
	require.NoError(err)

	proxies = CompleteProxyConfigurers(proxies)
	secondProxyJSON, err := json.Marshal(proxies[0])
	require.NoError(err)

	require.Equal(string(firstProxyJSON), string(secondProxyJSON))
}

func TestCompleteVisitorConfigurers_Idempotent(t *testing.T) {
	require := require.New(t)

	visitorCfg := &v1.XTCPVisitorConfig{}
	visitorCfg.Name = "visitor"
	visitorCfg.Type = "xtcp"
	visitorCfg.ServerName = "server"
	visitorCfg.SecretKey = "secret"
	visitorCfg.BindPort = 10081

	visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg})
	firstVisitorJSON, err := json.Marshal(visitors[0])
	require.NoError(err)

	visitors = CompleteVisitorConfigurers(visitors)
	secondVisitorJSON, err := json.Marshal(visitors[0])
	require.NoError(err)

	require.Equal(string(firstVisitorJSON), string(secondVisitorJSON))
}

func TestFilterClientConfigurers_FilterByStartAndEnabled(t *testing.T) {
	require := require.New(t)

	enabled := true
	disabled := false

	proxyKeep := &v1.TCPProxyConfig{}
	proxyKeep.Name = "keep"
	proxyKeep.Type = "tcp"
	proxyKeep.LocalPort = 10080
	proxyKeep.Enabled = &enabled

	proxyDropByStart := &v1.TCPProxyConfig{}
	proxyDropByStart.Name = "drop-by-start"
	proxyDropByStart.Type = "tcp"
	proxyDropByStart.LocalPort = 10081
	proxyDropByStart.Enabled = &enabled

	proxyDropByEnabled := &v1.TCPProxyConfig{}
	proxyDropByEnabled.Name = "drop-by-enabled"
	proxyDropByEnabled.Type = "tcp"
	proxyDropByEnabled.LocalPort = 10082
	proxyDropByEnabled.Enabled = &disabled

	common := &v1.ClientCommonConfig{
		Start: []string{"keep"},
	}

	proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{
		proxyKeep,
		proxyDropByStart,
		proxyDropByEnabled,
	}, nil)
	require.Len(visitors, 0)
	require.Len(proxies, 1)
	require.Equal("keep", proxies[0].GetBaseConfig().Name)
}

func TestLoadClientConfigResult_DuplicateNames(t *testing.T) {
	tests := []struct {
		name      string
		content   string
		errSubstr string
	}{
		{
			name: "duplicate proxy names",
			content: `
serverAddr = "127.0.0.1"
serverPort = 7000

[[proxies]]
name = "dup"
type = "tcp"
localPort = 22
remotePort = 6000

[[proxies]]
name = "dup"
type = "tcp"
localPort = 3306
remotePort = 6001
`,
			errSubstr: "proxy name [dup] is duplicated",
		},
		{
			name: "duplicate visitor names",
			content: `
serverAddr = "127.0.0.1"
serverPort = 7000

[[visitors]]
name = "dup"
type = "stcp"
serverName = "a"
secretKey = "secret"
bindPort = 9001

[[visitors]]
name = "dup"
type = "stcp"
serverName = "b"
secretKey = "secret"
bindPort = 9002
`,
			errSubstr: "visitor name [dup] is duplicated",
		},
		{
			name: "unique names",
			content: `
serverAddr = "127.0.0.1"
serverPort = 7000

[[proxies]]
name = "p1"
type = "tcp"
localPort = 22
remotePort = 6000

[[proxies]]
name = "p2"
type = "tcp"
localPort = 3306
remotePort = 6001
`,
		},
		{
			name: "same name across proxy and visitor",
			content: `
serverAddr = "127.0.0.1"
serverPort = 7000

[[proxies]]
name = "same"
type = "tcp"
localPort = 22
remotePort = 6000

[[visitors]]
name = "same"
type = "stcp"
serverName = "a"
secretKey = "secret"
bindPort = 9001
`,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			require := require.New(t)
			path := filepath.Join(t.TempDir(), "frpc.toml")
			require.NoError(os.WriteFile(path, []byte(tc.content), 0o600))

			_, err := LoadClientConfigResult(path, false)
			if tc.errSubstr == "" {
				require.NoError(err)
			} else {
				require.ErrorContains(err, tc.errSubstr)
			}
		})
	}
}

// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
func TestYAMLEdgeCases(t *testing.T) {
	require := require.New(t)

	// Test array at root (should fail for frp config)
	arrayYAML := []byte(`
- item1
- item2
`)
	clientCfg := v1.ClientConfig{}
	err := LoadConfigure(arrayYAML, &clientCfg, true)
	require.Error(err) // Should fail because ClientConfig expects an object

	// Test scalar at root (should fail for frp config)
	scalarYAML := []byte(`"just a string"`)
	err = LoadConfigure(scalarYAML, &clientCfg, true)
	require.Error(err) // Should fail because ClientConfig expects an object

	// Test empty object (should work)
	emptyYAML := []byte(`{}`)
	err = LoadConfigure(emptyYAML, &clientCfg, true)
	require.NoError(err)

	// Test nested structure without dots (should work)
	nestedYAML := []byte(`
serverAddr: "127.0.0.1"
serverPort: 7000
`)
	err = LoadConfigure(nestedYAML, &clientCfg, true)
	require.NoError(err)
	require.Equal("127.0.0.1", clientCfg.ServerAddr)
	require.Equal(7000, clientCfg.ServerPort)
}

func TestTOMLSyntaxErrorWithPosition(t *testing.T) {
	require := require.New(t)

	// TOML with syntax error (unclosed table array header)
	content := `serverAddr = "127.0.0.1"
serverPort = 7000

[[proxies]
name = "test"
`

	clientCfg := v1.ClientConfig{}
	err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
	require.Error(err)
	require.Contains(err.Error(), "toml")
	require.Contains(err.Error(), "line")
	require.Contains(err.Error(), "column")
}

func TestTOMLTypeMismatchErrorWithFieldInfo(t *testing.T) {
	require := require.New(t)

	// TOML with wrong type: proxies should be a table array, not a string
	content := `serverAddr = "127.0.0.1"
serverPort = 7000
proxies = "this should be a table array"
`

	clientCfg := v1.ClientConfig{}
	err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
	require.Error(err)
	// The error should contain field info
	errMsg := err.Error()
	require.Contains(errMsg, "proxies")
	require.NotContains(errMsg, "line")
}

func TestFindFieldLineInContent(t *testing.T) {
	content := []byte(`serverAddr = "127.0.0.1"
serverPort = 7000

[[proxies]]
name = "test"
type = "tcp"
remotePort = 6000
`)

	tests := []struct {
		fieldPath string
		wantLine  int
	}{
		{"serverAddr", 1},
		{"serverPort", 2},
		{"name", 5},
		{"type", 6},
		{"remotePort", 7},
		{"nonexistent", 0},
	}

	for _, tt := range tests {
		t.Run(tt.fieldPath, func(t *testing.T) {
			got := findFieldLineInContent(content, tt.fieldPath)
			require.Equal(t, tt.wantLine, got)
		})
	}
}

func TestFormatDetection(t *testing.T) {
	tests := []struct {
		path   string
		format string
	}{
		{"config.toml", "toml"},
		{"config.TOML", "toml"},
		{"config.yaml", "yaml"},
		{"config.yml", "yaml"},
		{"config.json", "json"},
		{"config.ini", ""},
		{"config", ""},
	}

	for _, tt := range tests {
		t.Run(tt.path, func(t *testing.T) {
			require.Equal(t, tt.format, detectFormatFromPath(tt.path))
		})
	}
}

func TestValidTOMLStillWorks(t *testing.T) {
	require := require.New(t)

	// Valid TOML with format hint should work fine
	content := `serverAddr = "127.0.0.1"
serverPort = 7000

[[proxies]]
name = "test"
type = "tcp"
remotePort = 6000
`
	clientCfg := v1.ClientConfig{}
	err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
	require.NoError(err)
	require.Equal("127.0.0.1", clientCfg.ServerAddr)
	require.Equal(7000, clientCfg.ServerPort)
	require.Len(clientCfg.Proxies, 1)
}
