package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"io"
	"net/http"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestSevenDaySession(t *testing.T) {
	setup := setupTestServer(t)

	// Ensure test user exists (uses helper with auto-cleanup)
	userID := getTestUserID(t, setup.DB)
	_ = userID // Use userID to avoid linter warning

	t.Run("Login returns 7-day session", func(t *testing.T) {
		loginReq := map[string]string{
			"login":    "testuser@tests.bookhoard.internal",
			"password": "Test@Pass123!",
		}
		body, _ := json.Marshal(loginReq)

		req, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewReader(body))
		require.NoError(t, err)
		req.Header.Set("Content-Type", "application/json")

		client := &http.Client{}
		resp, err := client.Do(req)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(resp.Body)

		assert.Equal(t, http.StatusOK, resp.StatusCode)

		var loginResult map[string]interface{}
		err = json.NewDecoder(resp.Body).Decode(&loginResult)
		require.NoError(t, err)

		// Verify ExpiresIn is 7 days in seconds (604800)
		expiresIn, ok := loginResult["expires_in"].(float64)
		require.True(t, ok, "Should have expires_in")
		assert.Equal(t, float64(604800), expiresIn, "ExpiresIn should be 7 days (604800 seconds)")

		// Verify token cookie is set with Max-Age of 7 days
		cookies := resp.Cookies()
		var tokenCookie *http.Cookie
		for _, c := range cookies {
			if c.Name == "token" {
				tokenCookie = c
				break
			}
		}
		require.NotNil(t, tokenCookie, "Token cookie should be set")
		assert.Equal(t, 604800, tokenCookie.MaxAge, "Cookie Max-Age should be 7 days (604800 seconds)")

		// Verify access_token exists
		accessToken, ok := loginResult["access_token"].(string)
		require.True(t, ok, "Should have access_token")
		require.NotEmpty(t, accessToken, "Access token should not be empty")
	})

	t.Run("Refresh token returns 7-day access token", func(t *testing.T) {
		// First login to get refresh token
		loginReq := map[string]string{
			"login":    "testuser@tests.bookhoard.internal",
			"password": "Test@Pass123!",
		}
		loginBody, _ := json.Marshal(loginReq)

		loginHTTP, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewReader(loginBody))
		require.NoError(t, err)
		loginHTTP.Header.Set("Content-Type", "application/json")

		loginResp, err := http.DefaultClient.Do(loginHTTP)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(loginResp.Body)

		require.Equal(t, http.StatusOK, loginResp.StatusCode)

		var loginResult map[string]interface{}
		err = json.NewDecoder(loginResp.Body).Decode(&loginResult)
		require.NoError(t, err)

		refreshToken, ok := loginResult["refresh_token"].(string)
		require.True(t, ok, "Should have refresh_token")

		// Now use the refresh token
		refreshReq := map[string]interface{}{
			"refresh_token": refreshToken,
		}
		refreshBody, _ := json.Marshal(refreshReq)

		refreshHTTP, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/refresh", bytes.NewReader(refreshBody))
		require.NoError(t, err)
		refreshHTTP.Header.Set("Content-Type", "application/json")

		refreshResp, err := http.DefaultClient.Do(refreshHTTP)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(refreshResp.Body)

		assert.Equal(t, http.StatusOK, refreshResp.StatusCode)

		var refreshResult map[string]interface{}
		err = json.NewDecoder(refreshResp.Body).Decode(&refreshResult)
		require.NoError(t, err)

		// Verify ExpiresIn is 7 days
		expiresIn, ok := refreshResult["expires_in"].(float64)
		require.True(t, ok, "Should have expires_in in refresh response")
		assert.Equal(t, float64(604800), expiresIn, "Refreshed token ExpiresIn should be 7 days (604800 seconds)")

		// Verify access_token exists
		accessToken, ok := refreshResult["access_token"].(string)
		require.True(t, ok, "Should have access_token in refresh response")
		require.NotEmpty(t, accessToken, "Access token should not be empty")
	})

	t.Run("JWT token has 7-day expiration claim", func(t *testing.T) {
		loginReq := map[string]string{
			"login":    "testuser@tests.bookhoard.internal",
			"password": "Test@Pass123!",
		}
		body, _ := json.Marshal(loginReq)

		req, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", bytes.NewReader(body))
		require.NoError(t, err)
		req.Header.Set("Content-Type", "application/json")

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(resp.Body)

		var authResponse struct {
			Token string `json:"access_token"`
		}
		err = json.NewDecoder(resp.Body).Decode(&authResponse)
		require.NoError(t, err)

		// Decode JWT (simple split, in real code use proper JWT library)
		parts := strings.Split(authResponse.Token, ".")
		require.Len(t, parts, 3, "JWT should have 3 parts")

		// Decode payload (base64url encoded)
		payload := parts[1]

		decoded, err := base64.RawURLEncoding.DecodeString(payload)
		require.NoError(t, err, "Should be able to decode JWT payload")

		var claims struct {
			Exp int64 `json:"exp"`
			Iat int64 `json:"iat"`
		}
		err = json.Unmarshal(decoded, &claims)
		require.NoError(t, err)

		// Verify expiration is approximately 7 days from now
		now := time.Now().Unix()
		expectedExp := now + (7 * 24 * 60 * 60)
		timeDiff := claims.Exp - expectedExp

		assert.True(t, timeDiff > -10 && timeDiff < 10, "JWT exp claim should be approximately 7 days from now, got diff: %d", timeDiff)
	})
}

func Test401ErrorHandler(t *testing.T) {
	setup := setupTestServer(t)

	t.Run("HTML request redirects to login", func(t *testing.T) {
		req, err := http.NewRequest("GET", setup.Server.URL+"/dashboard", nil)
		require.NoError(t, err)
		req.Header.Set("Accept", "text/html")

		client := &http.Client{
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			},
		}
		resp, err := client.Do(req)
		require.NoError(t, err)

		// Should redirect to login with session=expired
		assert.Equal(t, http.StatusFound, resp.StatusCode)
		location := resp.Header.Get("Location")
		assert.Contains(t, location, "/login?session=expired", "Should redirect to login with session expired query param")
	})

	t.Run("API request returns JSON error", func(t *testing.T) {
		req, err := http.NewRequest("GET", setup.Server.URL+"/api/libraries/visible", nil)
		require.NoError(t, err)
		req.Header.Set("Accept", "application/json")

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)

		assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)

		var errorResponse struct {
			Error   string `json:"error"`
			Message string `json:"message"`
		}
		err = json.NewDecoder(resp.Body).Decode(&errorResponse)
		require.NoError(t, err)

		assert.Equal(t, "session_expired", errorResponse.Error)
		assert.Contains(t, errorResponse.Message, "session has expired")
	})
}

func TestNoClientSideCookies(t *testing.T) {
	setup := setupTestServer(t)

	t.Run("Register does not set document.cookie", func(t *testing.T) {
		formData := url.Values{}
		formData.Set("email", "nocookie@example.com")
		formData.Set("username", "nocookie")
		formData.Set("password", "TestPassword123!")
		formData.Set("first_name", "No")
		formData.Set("last_name", "Cookie")

		req, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/register", strings.NewReader(formData.Encode()))
		require.NoError(t, err)
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("HX-Request", "true")

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		_ = resp.Body.Close()

		// Accept both 201 (new user) and 409 (already exists from previous run)
		require.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusConflict,
			"Registration should return 201 or 409")

		// Only check document.cookie if we got a successful response
		if resp.StatusCode == http.StatusCreated {
			// Re-read body since we already closed it
			formData2 := url.Values{}
			formData2.Set("email", "nocookie3@example.com")
			formData2.Set("username", "nocookie3")
			formData2.Set("password", "TestPassword123!")
			formData2.Set("first_name", "No")
			formData2.Set("last_name", "Cookie3")

			req2, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/register", strings.NewReader(formData2.Encode()))
			require.NoError(t, err)
			req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
			req2.Header.Set("HX-Request", "true")

			resp2, err := http.DefaultClient.Do(req2)
			require.NoError(t, err)
			defer func(Body io.ReadCloser) {
				_ = Body.Close()
			}(resp2.Body)

			body, err := io.ReadAll(resp2.Body)
			require.NoError(t, err)
			bodyStr := string(body)

			// Verify document.cookie is NOT set in the response
			assert.NotContains(t, bodyStr, "document.cookie", "Response should not contain document.cookie")
		}
	})

	t.Run("Login does not set document.cookie", func(t *testing.T) {
		// Use the standard test user
		loginData := url.Values{}
		loginData.Set("login", "testuser@tests.bookhoard.internal")
		loginData.Set("password", "Test@Pass123!")

		loginReq, err := http.NewRequest("POST", setup.Server.URL+"/api/auth/login", strings.NewReader(loginData.Encode()))
		require.NoError(t, err)
		loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		loginReq.Header.Set("HX-Request", "true")

		loginResp, err := http.DefaultClient.Do(loginReq)
		require.NoError(t, err)
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(loginResp.Body)

		assert.Equal(t, http.StatusOK, loginResp.StatusCode)

		body, err := io.ReadAll(loginResp.Body)
		require.NoError(t, err)
		bodyStr := string(body)

		// Verify document.cookie is NOT set in the response
		assert.NotContains(t, bodyStr, "document.cookie", "Login response should not contain document.cookie")
	})
}
