package application

import (
	"errors"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"goauthentik.io/internal/outpost/proxyv2/constants"
	"goauthentik.io/internal/outpost/proxyv2/types"
)

func TestLogout(t *testing.T) {
	a := newTestApplication()
	_ = a.configureProxy()
	req, _ := http.NewRequest("GET", "https://ext.t.goauthentik.io/foo", nil)
	rr := httptest.NewRecorder()

	// Login once
	s, _ := a.sessions.Get(req, a.SessionName())
	s.ID = uuid.New().String()
	s.Options.MaxAge = 86400
	s.Values[constants.SessionClaims] = types.Claims{
		Sub: "foo",
	}
	err := a.sessions.Save(req, rr, s)
	if err != nil {
		panic(err)
	}

	a.mux.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusBadGateway, rr.Code)

	// Login twice
	s2, _ := a.sessions.Get(req, a.SessionName())
	s2.ID = uuid.New().String()
	s2.Options.MaxAge = 86400
	s2.Values[constants.SessionClaims] = types.Claims{
		Sub: "foo",
	}
	err = a.sessions.Save(req, rr, s2)
	if err != nil {
		panic(err)
	}

	a.mux.ServeHTTP(rr, req)

	assert.Equal(t, http.StatusBadGateway, rr.Code)

	// Logout
	req, _ = http.NewRequest("GET", "https://ext.t.goauthentik.io/outpost.goauthentik.io/sign_out", nil)
	s3, _ := a.sessions.Get(req, a.SessionName())
	s3.ID = uuid.New().String()
	s3.Options.MaxAge = 86400
	s3.Values[constants.SessionClaims] = types.Claims{
		Sub: "foo",
	}
	err = a.sessions.Save(req, rr, s3)
	if err != nil {
		panic(err)
	}

	rr = httptest.NewRecorder()
	a.handleSignOut(rr, req)
	assert.Equal(t, http.StatusFound, rr.Code)

	s1Name := filepath.Join(os.TempDir(), "session_"+s.ID)
	_, err = os.Stat(s1Name)
	assert.True(t, errors.Is(err, os.ErrNotExist))
	s2Name := filepath.Join(os.TempDir(), "session_"+s2.ID)
	_, err = os.Stat(s2Name)
	assert.True(t, errors.Is(err, os.ErrNotExist))
}

func TestStaleCookieDeletion(t *testing.T) {
	a := newTestApplication()
	_ = a.configureProxy()

	// Create a request with a session cookie that references a non-existent session file
	req, _ := http.NewRequest("GET", "https://ext.t.goauthentik.io/foo", nil)

	// Set a cookie for a session that doesn't exist (simulates pod restart)
	nonExistentSessionID := uuid.New().String()
	req.AddCookie(&http.Cookie{
		Name:  a.SessionName(),
		Value: "encoded_session_data_" + nonExistentSessionID,
		Path:  "/",
	})

	rr := httptest.NewRecorder()

	// Call getClaimsFromSession which should delete the stale cookie
	claims := a.getClaimsFromSession(rr, req)

	// Verify no claims were returned (session doesn't exist)
	assert.Nil(t, claims)

	// Verify the response includes a Set-Cookie header to delete the stale cookie
	cookies := rr.Result().Cookies()
	var foundDeleteCookie bool
	for _, cookie := range cookies {
		if cookie.Name == a.SessionName() && cookie.MaxAge < 0 {
			foundDeleteCookie = true
			break
		}
	}
	assert.True(t, foundDeleteCookie, "Expected stale session cookie to be deleted")
}

func TestStateFromRequestDeletesStaleCookie(t *testing.T) {
	a := newTestApplication()
	_ = a.configureProxy()

	// Create a valid state JWT (from createState)
	req, _ := http.NewRequest("GET", "https://ext.t.goauthentik.io/foo", nil)
	rr := httptest.NewRecorder()

	state, err := a.createState(req, rr, "/redirect")
	assert.NoError(t, err)

	// Create a new request with the state but a stale session cookie
	req2, _ := http.NewRequest("GET", "https://ext.t.goauthentik.io/callback?state="+state, nil)

	// Add a cookie for a non-existent session
	nonExistentSessionID := uuid.New().String()
	req2.AddCookie(&http.Cookie{
		Name:  a.SessionName(),
		Value: "encoded_session_data_" + nonExistentSessionID,
		Path:  "/",
	})

	rr2 := httptest.NewRecorder()

	// Call stateFromRequest which should fail due to missing session
	// but should also delete the stale cookie
	claims := a.stateFromRequest(rr2, req2)

	// Verify no claims were returned
	assert.Nil(t, claims)

	// Verify the response includes a Set-Cookie header to delete the stale cookie
	cookies := rr2.Result().Cookies()
	var foundDeleteCookie bool
	for _, cookie := range cookies {
		if cookie.Name == a.SessionName() && cookie.MaxAge < 0 {
			foundDeleteCookie = true
			break
		}
	}
	assert.True(t, foundDeleteCookie, "Expected stale session cookie to be deleted")
}

func TestCreateStateWithStaleCookie(t *testing.T) {
	a := newTestApplication()
	_ = a.configureProxy()

	// Create a request with a stale session cookie (simulates outpost restart or user change)
	req, _ := http.NewRequest("GET", "https://ext.t.goauthentik.io/outpost.goauthentik.io/start", nil)

	// Add a cookie for a non-existent session
	nonExistentSessionID := uuid.New().String()
	req.AddCookie(&http.Cookie{
		Name:  a.SessionName(),
		Value: "encoded_session_data_" + nonExistentSessionID,
		Path:  "/",
	})

	rr := httptest.NewRecorder()

	// Call createState which should succeed despite the stale cookie
	state, err := a.createState(req, rr, "/redirect")

	// Verify createState succeeded
	assert.NoError(t, err)
	assert.NotEmpty(t, state)

	// Verify the response includes a Set-Cookie header to delete the stale cookie
	cookies := rr.Result().Cookies()
	var foundDeleteCookie bool
	for _, cookie := range cookies {
		if cookie.Name == a.SessionName() && cookie.MaxAge < 0 {
			foundDeleteCookie = true
			break
		}
	}
	assert.True(t, foundDeleteCookie, "Expected stale session cookie to be deleted")
}
