// Copyright 2025 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 basic

import (
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"github.com/onsi/ginkgo/v2"

	"github.com/fatedier/frp/test/e2e/framework"
	"github.com/fatedier/frp/test/e2e/framework/consts"
	"github.com/fatedier/frp/test/e2e/pkg/port"
)

var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
	f := framework.NewDefaultFramework()

	createExecTokenScript := func(name string) string {
		scriptPath := filepath.Join(f.TempDirectory, name)
		scriptContent := `#!/bin/sh
printf '%s\n' "$1"
`
		err := os.WriteFile(scriptPath, []byte(scriptContent), 0o600)
		framework.ExpectNoError(err)
		err = os.Chmod(scriptPath, 0o700)
		framework.ExpectNoError(err)
		return scriptPath
	}

	ginkgo.Describe("File-based token loading", func() {
		ginkgo.It("should work with file tokenSource", func() {
			// Create a temporary token file
			tmpDir := f.TempDirectory
			tokenFile := filepath.Join(tmpDir, "test_token")
			tokenContent := "test-token-123"

			err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600)
			framework.ExpectNoError(err)

			serverConf := consts.DefaultServerConfig
			clientConf := consts.DefaultClientConfig

			portName := port.GenName("TCP")

			// Server config with tokenSource
			serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, tokenFile)

			// Client config with matching token
			clientConf += fmt.Sprintf(`
auth.token = "%s"

[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, tokenContent, framework.TCPEchoServerPort, portName)

			f.RunProcesses(serverConf, []string{clientConf})

			framework.NewRequestExpect(f).PortName(portName).Ensure()
		})

		ginkgo.It("should work with client tokenSource", func() {
			// Create a temporary token file
			tmpDir := f.TempDirectory
			tokenFile := filepath.Join(tmpDir, "client_token")
			tokenContent := "client-token-456"

			err := os.WriteFile(tokenFile, []byte(tokenContent), 0o600)
			framework.ExpectNoError(err)

			serverConf := consts.DefaultServerConfig
			clientConf := consts.DefaultClientConfig

			portName := port.GenName("TCP")

			// Server config with matching token
			serverConf += fmt.Sprintf(`
auth.token = "%s"
`, tokenContent)

			// Client config with tokenSource
			clientConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"

[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, tokenFile, framework.TCPEchoServerPort, portName)

			f.RunProcesses(serverConf, []string{clientConf})

			framework.NewRequestExpect(f).PortName(portName).Ensure()
		})

		ginkgo.It("should work with both server and client tokenSource", func() {
			// Create temporary token files
			tmpDir := f.TempDirectory
			serverTokenFile := filepath.Join(tmpDir, "server_token")
			clientTokenFile := filepath.Join(tmpDir, "client_token")
			tokenContent := "shared-token-789"

			err := os.WriteFile(serverTokenFile, []byte(tokenContent), 0o600)
			framework.ExpectNoError(err)

			err = os.WriteFile(clientTokenFile, []byte(tokenContent), 0o600)
			framework.ExpectNoError(err)

			serverConf := consts.DefaultServerConfig
			clientConf := consts.DefaultClientConfig

			portName := port.GenName("TCP")

			// Server config with tokenSource
			serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, serverTokenFile)

			// Client config with tokenSource
			clientConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"

[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, clientTokenFile, framework.TCPEchoServerPort, portName)

			f.RunProcesses(serverConf, []string{clientConf})

			framework.NewRequestExpect(f).PortName(portName).Ensure()
		})

		ginkgo.It("should fail with mismatched tokens", func() {
			// Create temporary token files with different content
			tmpDir := f.TempDirectory
			serverTokenFile := filepath.Join(tmpDir, "server_token")
			clientTokenFile := filepath.Join(tmpDir, "client_token")

			err := os.WriteFile(serverTokenFile, []byte("server-token"), 0o600)
			framework.ExpectNoError(err)

			err = os.WriteFile(clientTokenFile, []byte("client-token"), 0o600)
			framework.ExpectNoError(err)

			serverConf := consts.DefaultServerConfig
			clientConf := consts.DefaultClientConfig

			portName := port.GenName("TCP")

			// Server config with tokenSource
			serverConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, serverTokenFile)

			// Client config with different tokenSource
			clientConf += fmt.Sprintf(`
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"

[[proxies]]
name = "tcp"
type = "tcp"
localPort = {{ .%s }}
remotePort = {{ .%s }}
`, clientTokenFile, framework.TCPEchoServerPort, portName)

			f.RunProcesses(serverConf, []string{clientConf})

			// This should fail due to token mismatch - the client should not be able to connect
			// We expect the request to fail because the proxy tunnel is not established
			framework.NewRequestExpect(f).PortName(portName).ExpectError(true).Ensure()
		})

		ginkgo.It("should fail with non-existent token file", func() {
			tmpDir := f.TempDirectory
			nonExistentFile := filepath.Join(tmpDir, "non_existent_token")

			serverPort := f.AllocPort()
			serverConf := fmt.Sprintf(`
bindAddr = "0.0.0.0"
bindPort = %d
auth.tokenSource.type = "file"
auth.tokenSource.file.path = "%s"
`, serverPort, nonExistentFile)

			serverConfigPath := f.GenerateConfigFile(serverConf)

			_, _, _ = f.RunFrps("-c", serverConfigPath)

			// Server should have failed to start, so the port should not be listening.
			conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(serverPort)), 1*time.Second)
			if err == nil {
				conn.Close()
			}
			framework.ExpectTrue(err != nil, "server should not be listening on port %d", serverPort)
		})
	})

	ginkgo.Describe("Exec-based token loading", func() {
		ginkgo.It("should work with server tokenSource", func() {
			execValue := "exec-server-value"
			scriptPath := createExecTokenScript("server_token_exec.sh")

			serverPort := f.AllocPort()
			remotePort := f.AllocPort()

			serverConf := fmt.Sprintf(`
bindAddr = "0.0.0.0"
bindPort = %d

auth.tokenSource.type = "exec"
auth.tokenSource.exec.command = %q
auth.tokenSource.exec.args = [%q]
`, serverPort, scriptPath, execValue)

			clientConf := fmt.Sprintf(`
serverAddr = "127.0.0.1"
serverPort = %d
loginFailExit = false
auth.token = %q

[[proxies]]
name = "tcp"
type = "tcp"
localPort = %d
remotePort = %d
`, serverPort, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)

			serverConfigPath := f.GenerateConfigFile(serverConf)
			clientConfigPath := f.GenerateConfigFile(clientConf)

			_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
			framework.ExpectNoError(err)

			_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
			framework.ExpectNoError(err)

			framework.NewRequestExpect(f).Port(remotePort).Ensure()
		})

		ginkgo.It("should work with client tokenSource", func() {
			execValue := "exec-client-value"
			scriptPath := createExecTokenScript("client_token_exec.sh")

			serverPort := f.AllocPort()
			remotePort := f.AllocPort()

			serverConf := fmt.Sprintf(`
bindAddr = "0.0.0.0"
bindPort = %d

auth.token = %q
`, serverPort, execValue)

			clientConf := fmt.Sprintf(`
serverAddr = "127.0.0.1"
serverPort = %d
loginFailExit = false

auth.tokenSource.type = "exec"
auth.tokenSource.exec.command = %q
auth.tokenSource.exec.args = [%q]

[[proxies]]
name = "tcp"
type = "tcp"
localPort = %d
remotePort = %d
`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)

			serverConfigPath := f.GenerateConfigFile(serverConf)
			clientConfigPath := f.GenerateConfigFile(clientConf)

			_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
			framework.ExpectNoError(err)

			_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
			framework.ExpectNoError(err)

			framework.NewRequestExpect(f).Port(remotePort).Ensure()
		})

		ginkgo.It("should work with both server and client tokenSource", func() {
			execValue := "exec-shared-value"
			scriptPath := createExecTokenScript("shared_token_exec.sh")

			serverPort := f.AllocPort()
			remotePort := f.AllocPort()

			serverConf := fmt.Sprintf(`
bindAddr = "0.0.0.0"
bindPort = %d

auth.tokenSource.type = "exec"
auth.tokenSource.exec.command = %q
auth.tokenSource.exec.args = [%q]
`, serverPort, scriptPath, execValue)

			clientConf := fmt.Sprintf(`
serverAddr = "127.0.0.1"
serverPort = %d
loginFailExit = false

auth.tokenSource.type = "exec"
auth.tokenSource.exec.command = %q
auth.tokenSource.exec.args = [%q]

[[proxies]]
name = "tcp"
type = "tcp"
localPort = %d
remotePort = %d
`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)

			serverConfigPath := f.GenerateConfigFile(serverConf)
			clientConfigPath := f.GenerateConfigFile(clientConf)

			_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
			framework.ExpectNoError(err)

			_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
			framework.ExpectNoError(err)

			framework.NewRequestExpect(f).Port(remotePort).Ensure()
		})

		ginkgo.It("should fail validation without allow-unsafe", func() {
			execValue := "exec-unsafe-value"
			scriptPath := createExecTokenScript("unsafe_token_exec.sh")

			serverPort := f.AllocPort()
			serverConf := fmt.Sprintf(`
bindAddr = "0.0.0.0"
bindPort = %d

auth.tokenSource.type = "exec"
auth.tokenSource.exec.command = %q
auth.tokenSource.exec.args = [%q]
`, serverPort, scriptPath, execValue)

			serverConfigPath := f.GenerateConfigFile(serverConf)

			_, output, err := f.RunFrps("verify", "-c", serverConfigPath)
			framework.ExpectNoError(err)
			framework.ExpectContainSubstring(output, "unsafe feature \"TokenSourceExec\" is not enabled")
		})
	})
})
