//go:build testing
// +build testing

package hub_test

import (
	"bytes"
	"crypto/ed25519"
	"encoding/json"
	"encoding/pem"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/henrygd/beszel/internal/migrations"
	beszelTests "github.com/henrygd/beszel/internal/tests"

	"github.com/pocketbase/pocketbase/core"
	pbTests "github.com/pocketbase/pocketbase/tests"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/crypto/ssh"
)

// marshal to json and return an io.Reader (for use in ApiScenario.Body)
func jsonReader(v any) io.Reader {
	data, err := json.Marshal(v)
	if err != nil {
		panic(err)
	}
	return bytes.NewReader(data)
}

func TestMakeLink(t *testing.T) {
	hub, _ := beszelTests.NewTestHub(t.TempDir())

	tests := []struct {
		name     string
		appURL   string
		parts    []string
		expected string
	}{
		{
			name:     "no parts, no trailing slash in AppURL",
			appURL:   "http://localhost:8090",
			parts:    []string{},
			expected: "http://localhost:8090",
		},
		{
			name:     "no parts, with trailing slash in AppURL",
			appURL:   "http://localhost:8090/",
			parts:    []string{},
			expected: "http://localhost:8090", // TrimSuffix should handle the trailing slash
		},
		{
			name:     "one part",
			appURL:   "http://example.com",
			parts:    []string{"one"},
			expected: "http://example.com/one",
		},
		{
			name:     "multiple parts",
			appURL:   "http://example.com",
			parts:    []string{"alpha", "beta", "gamma"},
			expected: "http://example.com/alpha/beta/gamma",
		},
		{
			name:     "parts with spaces needing escaping",
			appURL:   "http://example.com",
			parts:    []string{"path with spaces", "another part"},
			expected: "http://example.com/path%20with%20spaces/another%20part",
		},
		{
			name:     "parts with slashes needing escaping",
			appURL:   "http://example.com",
			parts:    []string{"a/b", "c"},
			expected: "http://example.com/a%2Fb/c", // url.PathEscape escapes '/'
		},
		{
			name:     "AppURL with subpath, no trailing slash",
			appURL:   "http://localhost/sub",
			parts:    []string{"resource"},
			expected: "http://localhost/sub/resource",
		},
		{
			name:     "AppURL with subpath, with trailing slash",
			appURL:   "http://localhost/sub/",
			parts:    []string{"item"},
			expected: "http://localhost/sub/item",
		},
		{
			name:     "empty parts in the middle",
			appURL:   "http://localhost",
			parts:    []string{"first", "", "third"},
			expected: "http://localhost/first/third",
		},
		{
			name:     "leading and trailing empty parts",
			appURL:   "http://localhost",
			parts:    []string{"", "path", ""},
			expected: "http://localhost/path",
		},
		{
			name:     "parts with various special characters",
			appURL:   "https://test.dev/",
			parts:    []string{"p@th?", "key=value&"},
			expected: "https://test.dev/p@th%3F/key=value&",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Store original app URL and restore it after the test
			originalAppURL := hub.Settings().Meta.AppURL
			hub.Settings().Meta.AppURL = tt.appURL
			defer func() { hub.Settings().Meta.AppURL = originalAppURL }()

			got := hub.MakeLink(tt.parts...)
			assert.Equal(t, tt.expected, got, "MakeLink generated URL does not match expected")
		})
	}
}

func TestGetSSHKey(t *testing.T) {
	hub, _ := beszelTests.NewTestHub(t.TempDir())

	// Test Case 1: Key generation (no existing key)
	t.Run("KeyGeneration", func(t *testing.T) {
		tempDir := t.TempDir()

		// Ensure pubKey is initially empty or different to ensure GetSSHKey sets it
		hub.SetPubkey("")

		signer, err := hub.GetSSHKey(tempDir)
		assert.NoError(t, err, "GetSSHKey should not error when generating a new key")
		assert.NotNil(t, signer, "GetSSHKey should return a non-nil signer")

		// Check if private key file was created
		privateKeyPath := filepath.Join(tempDir, "id_ed25519")
		info, err := os.Stat(privateKeyPath)
		assert.NoError(t, err, "Private key file should be created")
		assert.False(t, info.IsDir(), "Private key path should be a file, not a directory")

		// Check if h.pubKey was set
		assert.NotEmpty(t, hub.GetPubkey(), "h.pubKey should be set after key generation")
		assert.True(t, strings.HasPrefix(hub.GetPubkey(), "ssh-ed25519 "), "h.pubKey should start with 'ssh-ed25519 '")

		// Verify the generated private key is parsable
		keyData, err := os.ReadFile(privateKeyPath)
		require.NoError(t, err)
		_, err = ssh.ParsePrivateKey(keyData)
		assert.NoError(t, err, "Generated private key should be parsable by ssh.ParsePrivateKey")
	})

	// Test Case 2: Existing key
	t.Run("ExistingKey", func(t *testing.T) {
		tempDir := t.TempDir()

		// Manually create a valid key pair for the test
		rawPubKey, rawPrivKey, err := ed25519.GenerateKey(nil)
		require.NoError(t, err, "Failed to generate raw ed25519 key pair for pre-existing key test")

		// Marshal the private key into OpenSSH PEM format
		pemBlock, err := ssh.MarshalPrivateKey(rawPrivKey, "")
		require.NoError(t, err, "Failed to marshal private key to PEM block for pre-existing key test")

		privateKeyBytes := pem.EncodeToMemory(pemBlock)
		require.NotNil(t, privateKeyBytes, "PEM encoded private key bytes should not be nil")

		privateKeyPath := filepath.Join(tempDir, "id_ed25519")
		err = os.WriteFile(privateKeyPath, privateKeyBytes, 0600)
		require.NoError(t, err, "Failed to write pre-existing private key")

		// Determine the expected public key string
		sshPubKey, err := ssh.NewPublicKey(rawPubKey)
		require.NoError(t, err)
		expectedPubKeyStr := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPubKey)))

		// Reset h.pubKey to ensure it's set by GetSSHKey from the file
		hub.SetPubkey("")

		signer, err := hub.GetSSHKey(tempDir)
		assert.NoError(t, err, "GetSSHKey should not error when reading an existing key")
		assert.NotNil(t, signer, "GetSSHKey should return a non-nil signer for an existing key")

		// Check if h.pubKey was set correctly to the public key from the file
		assert.Equal(t, expectedPubKeyStr, hub.GetPubkey(), "h.pubKey should match the existing public key")

		// Verify the signer's public key matches the original public key
		signerPubKey := signer.PublicKey()
		marshaledSignerPubKey := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signerPubKey)))
		assert.Equal(t, expectedPubKeyStr, marshaledSignerPubKey, "Signer's public key should match the existing public key")
	})

	// Test Case 3: Error cases
	t.Run("ErrorCases", func(t *testing.T) {
		tests := []struct {
			name       string
			setupFunc  func(dir string) error
			errorCheck func(t *testing.T, err error)
		}{
			{
				name: "CorruptedKey",
				setupFunc: func(dir string) error {
					return os.WriteFile(filepath.Join(dir, "id_ed25519"), []byte("this is not a valid SSH key"), 0600)
				},
				errorCheck: func(t *testing.T, err error) {
					assert.Error(t, err)
					assert.Contains(t, err.Error(), "ssh: no key found")
				},
			},
			{
				name: "PermissionDenied",
				setupFunc: func(dir string) error {
					// Create the key file
					keyPath := filepath.Join(dir, "id_ed25519")
					if err := os.WriteFile(keyPath, []byte("dummy content"), 0600); err != nil {
						return err
					}
					// Make it read-only (can't be opened for writing in case a new key needs to be written)
					return os.Chmod(keyPath, 0400)
				},
				errorCheck: func(t *testing.T, err error) {
					// On read-only key, the parser will attempt to parse it and fail with "ssh: no key found"
					assert.Error(t, err)
				},
			},
			{
				name: "EmptyFile",
				setupFunc: func(dir string) error {
					// Create an empty file
					return os.WriteFile(filepath.Join(dir, "id_ed25519"), []byte{}, 0600)
				},
				errorCheck: func(t *testing.T, err error) {
					assert.Error(t, err)
					// The error from attempting to parse an empty file
					assert.Contains(t, err.Error(), "ssh: no key found")
				},
			},
		}

		for _, tc := range tests {
			t.Run(tc.name, func(t *testing.T) {
				tempDir := t.TempDir()

				// Setup the test case
				err := tc.setupFunc(tempDir)
				require.NoError(t, err, "Setup failed")

				// Reset h.pubKey before each test case
				hub.SetPubkey("")

				// Attempt to get SSH key
				_, err = hub.GetSSHKey(tempDir)

				// Verify the error
				tc.errorCheck(t, err)

				// Check that pubKey was not set in error cases
				assert.Empty(t, hub.GetPubkey(), "h.pubKey should not be set if there was an error")
			})
		}
	})
}

func TestApiRoutesAuthentication(t *testing.T) {
	hub, _ := beszelTests.NewTestHub(t.TempDir())
	defer hub.Cleanup()

	hub.StartHub()

	// Create test user and get auth token
	user, err := beszelTests.CreateUser(hub, "testuser@example.com", "password123")
	require.NoError(t, err, "Failed to create test user")

	adminUser, err := beszelTests.CreateRecord(hub, "users", map[string]any{
		"email":    "admin@example.com",
		"password": "password123",
		"role":     "admin",
	})
	require.NoError(t, err, "Failed to create admin user")
	adminUserToken, err := adminUser.NewAuthToken()

	// superUser, err := beszelTests.CreateRecord(hub, core.CollectionNameSuperusers, map[string]any{
	// 	"email":    "superuser@example.com",
	// 	"password": "password123",
	// })
	// require.NoError(t, err, "Failed to create superuser")

	userToken, err := user.NewAuthToken()
	require.NoError(t, err, "Failed to create auth token")

	// Create test system for user-alerts endpoints
	system, err := beszelTests.CreateRecord(hub, "systems", map[string]any{
		"name":  "test-system",
		"users": []string{user.Id},
		"host":  "127.0.0.1",
	})
	require.NoError(t, err, "Failed to create test system")

	testAppFactory := func(t testing.TB) *pbTests.TestApp {
		return hub.TestApp
	}

	scenarios := []beszelTests.ApiScenario{
		// Auth Protected Routes - Should require authentication
		{
			Name:            "POST /test-notification - no auth should fail",
			Method:          http.MethodPost,
			URL:             "/api/beszel/test-notification",
			ExpectedStatus:  401,
			ExpectedContent: []string{"requires valid"},
			TestAppFactory:  testAppFactory,
			Body: jsonReader(map[string]any{
				"url": "generic://127.0.0.1",
			}),
		},
		{
			Name:           "POST /test-notification - with auth should succeed",
			Method:         http.MethodPost,
			URL:            "/api/beszel/test-notification",
			TestAppFactory: testAppFactory,
			Headers: map[string]string{
				"Authorization": userToken,
			},
			Body: jsonReader(map[string]any{
				"url": "generic://127.0.0.1",
			}),
			ExpectedStatus:  200,
			ExpectedContent: []string{"sending message"},
		},
		{
			Name:            "GET /config-yaml - no auth should fail",
			Method:          http.MethodGet,
			URL:             "/api/beszel/config-yaml",
			ExpectedStatus:  401,
			ExpectedContent: []string{"requires valid"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:   "GET /config-yaml - with user auth should fail",
			Method: http.MethodGet,
			URL:    "/api/beszel/config-yaml",
			Headers: map[string]string{
				"Authorization": userToken,
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{"Requires admin"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:   "GET /config-yaml - with admin auth should succeed",
			Method: http.MethodGet,
			URL:    "/api/beszel/config-yaml",
			Headers: map[string]string{
				"Authorization": adminUserToken,
			},
			ExpectedStatus:  200,
			ExpectedContent: []string{"test-system"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:            "GET /universal-token - no auth should fail",
			Method:          http.MethodGet,
			URL:             "/api/beszel/universal-token",
			ExpectedStatus:  401,
			ExpectedContent: []string{"requires valid"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:   "GET /universal-token - with auth should succeed",
			Method: http.MethodGet,
			URL:    "/api/beszel/universal-token",
			Headers: map[string]string{
				"Authorization": userToken,
			},
			ExpectedStatus:  200,
			ExpectedContent: []string{"active", "token"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:            "POST /user-alerts - no auth should fail",
			Method:          http.MethodPost,
			URL:             "/api/beszel/user-alerts",
			ExpectedStatus:  401,
			ExpectedContent: []string{"requires valid"},
			TestAppFactory:  testAppFactory,
			Body: jsonReader(map[string]any{
				"name":    "CPU",
				"value":   80,
				"min":     10,
				"systems": []string{system.Id},
			}),
		},
		{
			Name:   "POST /user-alerts - with auth should succeed",
			Method: http.MethodPost,
			URL:    "/api/beszel/user-alerts",
			Headers: map[string]string{
				"Authorization": userToken,
			},
			ExpectedStatus:  200,
			ExpectedContent: []string{"\"success\":true"},
			TestAppFactory:  testAppFactory,
			Body: jsonReader(map[string]any{
				"name":    "CPU",
				"value":   80,
				"min":     10,
				"systems": []string{system.Id},
			}),
		},
		{
			Name:            "DELETE /user-alerts - no auth should fail",
			Method:          http.MethodDelete,
			URL:             "/api/beszel/user-alerts",
			ExpectedStatus:  401,
			ExpectedContent: []string{"requires valid"},
			TestAppFactory:  testAppFactory,
			Body: jsonReader(map[string]any{
				"name":    "CPU",
				"systems": []string{system.Id},
			}),
		},
		{
			Name:   "DELETE /user-alerts - with auth should succeed",
			Method: http.MethodDelete,
			URL:    "/api/beszel/user-alerts",
			Headers: map[string]string{
				"Authorization": userToken,
			},
			ExpectedStatus:  200,
			ExpectedContent: []string{"\"success\":true"},
			TestAppFactory:  testAppFactory,
			Body: jsonReader(map[string]any{
				"name":    "CPU",
				"systems": []string{system.Id},
			}),
			BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
				// Create an alert to delete
				beszelTests.CreateRecord(app, "alerts", map[string]any{
					"name":   "CPU",
					"system": system.Id,
					"user":   user.Id,
					"value":  80,
					"min":    10,
				})
			},
		},

		// Auth Optional Routes - Should work without authentication
		{
			Name:            "GET /getkey - no auth should fail",
			Method:          http.MethodGet,
			URL:             "/api/beszel/getkey",
			ExpectedStatus:  401,
			ExpectedContent: []string{"requires valid"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:   "GET /getkey - with auth should also succeed",
			Method: http.MethodGet,
			URL:    "/api/beszel/getkey",
			Headers: map[string]string{
				"Authorization": userToken,
			},
			ExpectedStatus:  200,
			ExpectedContent: []string{"\"key\":", "\"v\":"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:            "GET /first-run - no auth should succeed",
			Method:          http.MethodGet,
			URL:             "/api/beszel/first-run",
			ExpectedStatus:  200,
			ExpectedContent: []string{"\"firstRun\":false"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:   "GET /first-run - with auth should also succeed",
			Method: http.MethodGet,
			URL:    "/api/beszel/first-run",
			Headers: map[string]string{
				"Authorization": userToken,
			},
			ExpectedStatus:  200,
			ExpectedContent: []string{"\"firstRun\":false"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:            "GET /agent-connect - no auth should succeed (websocket upgrade fails but route is accessible)",
			Method:          http.MethodGet,
			URL:             "/api/beszel/agent-connect",
			ExpectedStatus:  400,
			ExpectedContent: []string{},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:   "POST /test-notification - invalid auth token should fail",
			Method: http.MethodPost,
			URL:    "/api/beszel/test-notification",
			Body: jsonReader(map[string]any{
				"url": "generic://127.0.0.1",
			}),
			Headers: map[string]string{
				"Authorization": "invalid-token",
			},
			ExpectedStatus:  401,
			ExpectedContent: []string{"requires valid"},
			TestAppFactory:  testAppFactory,
		},
		{
			Name:   "POST /user-alerts - invalid auth token should fail",
			Method: http.MethodPost,
			URL:    "/api/beszel/user-alerts",
			Headers: map[string]string{
				"Authorization": "invalid-token",
			},
			ExpectedStatus:  401,
			ExpectedContent: []string{"requires valid"},
			TestAppFactory:  testAppFactory,
			Body: jsonReader(map[string]any{
				"name":    "CPU",
				"value":   80,
				"min":     10,
				"systems": []string{system.Id},
			}),
		},
	}

	for _, scenario := range scenarios {
		scenario.Test(t)
	}
}

func TestFirstUserCreation(t *testing.T) {
	t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) {
		hub, _ := beszelTests.NewTestHub(t.TempDir())
		defer hub.Cleanup()

		hub.StartHub()

		testAppFactoryExisting := func(t testing.TB) *pbTests.TestApp {
			return hub.TestApp
		}

		scenarios := []beszelTests.ApiScenario{
			{
				Name:   "POST /create-user - should be available when no users exist",
				Method: http.MethodPost,
				URL:    "/api/beszel/create-user",
				Body: jsonReader(map[string]any{
					"email":    "firstuser@example.com",
					"password": "password123",
				}),
				ExpectedStatus:  200,
				ExpectedContent: []string{"User created"},
				TestAppFactory:  testAppFactoryExisting,
				BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
					userCount, err := hub.CountRecords("users")
					require.NoError(t, err)
					require.Zero(t, userCount, "Should start with no users")
					superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
					require.NoError(t, err)
					require.EqualValues(t, 1, len(superusers), "Should start with one temporary superuser")
					require.EqualValues(t, migrations.TempAdminEmail, superusers[0].GetString("email"), "Should have created one temporary superuser")
				},
				AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
					userCount, err := hub.CountRecords("users")
					require.NoError(t, err)
					require.EqualValues(t, 1, userCount, "Should have created one user")
					superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
					require.NoError(t, err)
					require.EqualValues(t, 1, len(superusers), "Should have created one superuser")
					require.EqualValues(t, "firstuser@example.com", superusers[0].GetString("email"), "Should have created one superuser")
				},
			},
			{
				Name:   "POST /create-user - should not be available when users exist",
				Method: http.MethodPost,
				URL:    "/api/beszel/create-user",
				Body: jsonReader(map[string]any{
					"email":    "firstuser@example.com",
					"password": "password123",
				}),
				ExpectedStatus:  404,
				ExpectedContent: []string{"wasn't found"},
				TestAppFactory:  testAppFactoryExisting,
			},
		}

		for _, scenario := range scenarios {
			scenario.Test(t)
		}
	})

	t.Run("CreateUserEndpoint not available when USER_EMAIL, USER_PASSWORD are set", func(t *testing.T) {
		os.Setenv("BESZEL_HUB_USER_EMAIL", "me@example.com")
		os.Setenv("BESZEL_HUB_USER_PASSWORD", "password123")
		defer os.Unsetenv("BESZEL_HUB_USER_EMAIL")
		defer os.Unsetenv("BESZEL_HUB_USER_PASSWORD")

		hub, _ := beszelTests.NewTestHub(t.TempDir())
		defer hub.Cleanup()

		hub.StartHub()

		testAppFactory := func(t testing.TB) *pbTests.TestApp {
			return hub.TestApp
		}

		scenario := beszelTests.ApiScenario{
			Name:            "POST /create-user - should not be available when USER_EMAIL, USER_PASSWORD are set",
			Method:          http.MethodPost,
			URL:             "/api/beszel/create-user",
			ExpectedStatus:  404,
			ExpectedContent: []string{"wasn't found"},
			TestAppFactory:  testAppFactory,
			BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
				users, err := hub.FindAllRecords("users")
				require.NoError(t, err)
				require.EqualValues(t, 1, len(users), "Should start with one user")
				require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user")
				superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
				require.NoError(t, err)
				require.EqualValues(t, 1, len(superusers), "Should start with one superuser")
				require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser")
			},
			AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
				users, err := hub.FindAllRecords("users")
				require.NoError(t, err)
				require.EqualValues(t, 1, len(users), "Should still have one user")
				require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user")
				superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers)
				require.NoError(t, err)
				require.EqualValues(t, 1, len(superusers), "Should still have one superuser")
				require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser")
			},
		}

		scenario.Test(t)
	})
}

func TestCreateUserEndpointAvailability(t *testing.T) {
	t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) {
		hub, _ := beszelTests.NewTestHub(t.TempDir())
		defer hub.Cleanup()

		// Ensure no users exist
		userCount, err := hub.CountRecords("users")
		require.NoError(t, err)
		require.Zero(t, userCount, "Should start with no users")

		hub.StartHub()

		testAppFactory := func(t testing.TB) *pbTests.TestApp {
			return hub.TestApp
		}

		scenario := beszelTests.ApiScenario{
			Name:   "POST /create-user - should be available when no users exist",
			Method: http.MethodPost,
			URL:    "/api/beszel/create-user",
			Body: jsonReader(map[string]any{
				"email":    "firstuser@example.com",
				"password": "password123",
			}),
			ExpectedStatus:  200,
			ExpectedContent: []string{"User created"},
			TestAppFactory:  testAppFactory,
		}

		scenario.Test(t)

		// Verify user was created
		userCount, err = hub.CountRecords("users")
		require.NoError(t, err)
		require.EqualValues(t, 1, userCount, "Should have created one user")
	})

	t.Run("CreateUserEndpoint not available when users exist", func(t *testing.T) {
		hub, _ := beszelTests.NewTestHub(t.TempDir())
		defer hub.Cleanup()

		// Create a user first
		_, err := beszelTests.CreateUser(hub, "existing@example.com", "password")
		require.NoError(t, err)

		hub.StartHub()

		testAppFactory := func(t testing.TB) *pbTests.TestApp {
			return hub.TestApp
		}

		scenario := beszelTests.ApiScenario{
			Name:   "POST /create-user - should not be available when users exist",
			Method: http.MethodPost,
			URL:    "/api/beszel/create-user",
			Body: jsonReader(map[string]any{
				"email":    "another@example.com",
				"password": "password123",
			}),
			ExpectedStatus:  404,
			ExpectedContent: []string{"wasn't found"},
			TestAppFactory:  testAppFactory,
		}

		scenario.Test(t)
	})
}
