package auth

import (
	"bytes"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"system-altrak/internal/domain"

	"github.com/gofiber/fiber/v2"
)

type authHandlerServiceMock struct {
	loginRememberMe bool
	loginUsername   string
	loginPassword   string
	loginUser       *domain.User
	loginAccess     string
	loginRefresh    string
	loginErr        error

	refreshToken      string
	refreshAccess     string
	refreshRefresh    string
	refreshPersistent bool
	refreshErr        error

	sessionUser   *domain.User
	sessionErr    error
	sessionUserID uint

	loginCalls   int
	refreshCalls int
	sessionCalls int
}

func (m *authHandlerServiceMock) Login(username, password string, rememberMe bool) (*domain.User, string, string, error) {
	m.loginCalls++
	m.loginUsername = username
	m.loginPassword = password
	m.loginRememberMe = rememberMe
	if m.loginErr != nil {
		return nil, "", "", m.loginErr
	}
	if m.loginUser == nil {
		m.loginUser = &domain.User{BaseModel: domain.BaseModel{ID: 1}, Username: username, Role: "admin", IsActive: true}
	}
	copyUser := *m.loginUser
	return &copyUser, m.loginAccess, m.loginRefresh, nil
}

func (m *authHandlerServiceMock) Refresh(refreshToken string) (string, string, bool, error) {
	m.refreshCalls++
	m.refreshToken = refreshToken
	if m.refreshErr != nil {
		return "", "", false, m.refreshErr
	}
	return m.refreshAccess, m.refreshRefresh, m.refreshPersistent, nil
}

func (m *authHandlerServiceMock) GetSession(userID uint) (*domain.User, error) {
	m.sessionCalls++
	m.sessionUserID = userID
	if m.sessionErr != nil {
		return nil, m.sessionErr
	}
	if m.sessionUser == nil {
		return nil, ErrInvalidSession
	}
	copyUser := *m.sessionUser
	return &copyUser, nil
}

func (m *authHandlerServiceMock) Logout(accessToken string, refreshToken string) error {
	return nil
}

func buildAuthHandlerTestApp(h *Handler) *fiber.App {
	app := fiber.New()
	app.Post("/login", h.Login)
	app.Post("/refresh", h.Refresh)
	return app
}

func buildAuthHandlerSessionTestApp(h *Handler, userID uint) *fiber.App {
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_id", userID)
		return c.Next()
	})
	app.Get("/session", h.Session)
	return app
}

func TestLoginSetsSessionCookiesWhenRememberMeDisabled(t *testing.T) {
	mock := &authHandlerServiceMock{
		loginUser:    &domain.User{BaseModel: domain.BaseModel{ID: 10}, Username: "alice", Role: "admin", IsActive: true},
		loginAccess:  "access-token",
		loginRefresh: "refresh-token",
	}
	h := NewHandler(mock, nil)
	app := buildAuthHandlerTestApp(h)

	reqBody := bytes.NewBufferString(`{"username":"alice","password":"secret","remember_me":false}`)
	req := httptest.NewRequest(http.MethodPost, "/login", reqBody)
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	if resp.StatusCode != fiber.StatusOK {
		t.Fatalf("expected %d, got %d", fiber.StatusOK, resp.StatusCode)
	}
	if mock.loginCalls != 1 || mock.loginRememberMe {
		t.Fatalf("unexpected login arguments calls=%d remember=%v", mock.loginCalls, mock.loginRememberMe)
	}

	cookies := strings.Join(resp.Header.Values("Set-Cookie"), "\n")
	if !strings.Contains(cookies, "access_token=access-token") || !strings.Contains(cookies, "refresh_token=refresh-token") {
		t.Fatalf("expected auth cookies to be set, got %q", cookies)
	}
	if strings.Contains(strings.ToLower(cookies), "expires=") {
		t.Fatalf("did not expect persistent expires attribute for session login, got %q", cookies)
	}
}

func TestLoginSetsPersistentCookiesWhenRememberMeEnabled(t *testing.T) {
	mock := &authHandlerServiceMock{
		loginUser:    &domain.User{BaseModel: domain.BaseModel{ID: 11}, Username: "bob", Role: "admin", IsActive: true},
		loginAccess:  "access-token-2",
		loginRefresh: "refresh-token-2",
	}
	h := NewHandler(mock, nil)
	app := buildAuthHandlerTestApp(h)

	reqBody := bytes.NewBufferString(`{"username":"bob","password":"secret","remember_me":true}`)
	req := httptest.NewRequest(http.MethodPost, "/login", reqBody)
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	if resp.StatusCode != fiber.StatusOK {
		t.Fatalf("expected %d, got %d", fiber.StatusOK, resp.StatusCode)
	}
	if mock.loginCalls != 1 || !mock.loginRememberMe {
		t.Fatalf("unexpected login arguments calls=%d remember=%v", mock.loginCalls, mock.loginRememberMe)
	}

	cookies := strings.Join(resp.Header.Values("Set-Cookie"), "\n")
	if !strings.Contains(strings.ToLower(cookies), "expires=") {
		t.Fatalf("expected persistent cookies to include expiry, got %q", cookies)
	}
}

func TestLoginTrimsUsernameBeforeServiceCall(t *testing.T) {
	mock := &authHandlerServiceMock{
		loginUser:    &domain.User{BaseModel: domain.BaseModel{ID: 12}, Username: "alice", Role: "admin", IsActive: true},
		loginAccess:  "access-token-3",
		loginRefresh: "refresh-token-3",
	}
	h := NewHandler(mock, nil)
	app := buildAuthHandlerTestApp(h)

	reqBody := bytes.NewBufferString(`{"username":"  alice  ","password":"secret","remember_me":false}`)
	req := httptest.NewRequest(http.MethodPost, "/login", reqBody)
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	if resp.StatusCode != fiber.StatusOK {
		t.Fatalf("expected %d, got %d", fiber.StatusOK, resp.StatusCode)
	}
	if mock.loginUsername != "alice" {
		t.Fatalf("expected trimmed username passed to service, got %q", mock.loginUsername)
	}
}

func TestLoginMapsInvalidCredentialsToUnauthorized(t *testing.T) {
	mock := &authHandlerServiceMock{loginErr: ErrInvalidCredentials}
	h := NewHandler(mock, nil)
	app := buildAuthHandlerTestApp(h)

	reqBody := bytes.NewBufferString(`{"username":"alice","password":"wrong","remember_me":false}`)
	req := httptest.NewRequest(http.MethodPost, "/login", reqBody)
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	if resp.StatusCode != fiber.StatusUnauthorized {
		t.Fatalf("expected %d, got %d", fiber.StatusUnauthorized, resp.StatusCode)
	}

	var body map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		t.Fatalf("failed to decode body: %v", err)
	}
	if body["message"] != "Username atau password salah" {
		t.Fatalf("unexpected message: %v", body["message"])
	}
}

func TestLoginMapsUnexpectedErrorsToInternalServerError(t *testing.T) {
	mock := &authHandlerServiceMock{loginErr: errors.New("database down")}
	h := NewHandler(mock, nil)
	app := buildAuthHandlerTestApp(h)

	reqBody := bytes.NewBufferString(`{"username":"alice","password":"secret","remember_me":false}`)
	req := httptest.NewRequest(http.MethodPost, "/login", reqBody)
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	if resp.StatusCode != fiber.StatusInternalServerError {
		t.Fatalf("expected %d, got %d", fiber.StatusInternalServerError, resp.StatusCode)
	}
}

func TestRefreshPropagatesPersistentCookies(t *testing.T) {
	mock := &authHandlerServiceMock{
		refreshAccess:     "refreshed-access",
		refreshRefresh:    "refreshed-refresh",
		refreshPersistent: true,
	}
	h := NewHandler(mock, nil)
	app := buildAuthHandlerTestApp(h)

	reqBody := bytes.NewBufferString(`{"refresh_token":"existing-refresh"}`)
	req := httptest.NewRequest(http.MethodPost, "/refresh", reqBody)
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	if resp.StatusCode != fiber.StatusOK {
		t.Fatalf("expected %d, got %d", fiber.StatusOK, resp.StatusCode)
	}
	if mock.refreshCalls != 1 || mock.refreshToken != "existing-refresh" {
		t.Fatalf("unexpected refresh arguments calls=%d token=%q", mock.refreshCalls, mock.refreshToken)
	}

	var body map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		t.Fatalf("failed to decode response body: %v", err)
	}
	if body["success"] != true {
		t.Fatalf("expected success response, got=%v", body["success"])
	}

	cookies := strings.Join(resp.Header.Values("Set-Cookie"), "\n")
	if !strings.Contains(strings.ToLower(cookies), "expires=") {
		t.Fatalf("expected refreshed persistent cookies to include expiry, got %q", cookies)
	}
}

func TestSessionReturnsCurrentUserPayload(t *testing.T) {
	mock := &authHandlerServiceMock{
		sessionUser: &domain.User{
			BaseModel: domain.BaseModel{ID: 77},
			Username:  "session-user",
			FullName:  "Session User",
			Role:      "admin",
			BranchID:  2,
		},
	}
	h := NewHandler(mock, nil)
	app := buildAuthHandlerSessionTestApp(h, 77)

	req := httptest.NewRequest(http.MethodGet, "/session", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	if resp.StatusCode != fiber.StatusOK {
		t.Fatalf("expected %d, got %d", fiber.StatusOK, resp.StatusCode)
	}
	if mock.sessionCalls != 1 || mock.sessionUserID != 77 {
		t.Fatalf("unexpected session arguments calls=%d userID=%d", mock.sessionCalls, mock.sessionUserID)
	}

	var body map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		t.Fatalf("failed to decode body: %v", err)
	}
	if body["success"] != true {
		t.Fatalf("expected success=true, got=%v", body["success"])
	}
	data, ok := body["data"].(map[string]interface{})
	if !ok {
		t.Fatalf("expected object data payload, got=%T", body["data"])
	}
	user, ok := data["user"].(map[string]interface{})
	if !ok {
		t.Fatalf("expected user object, got=%T", data["user"])
	}
	if user["username"] != "session-user" || user["role"] != "admin" {
		t.Fatalf("unexpected session payload: %+v", user)
	}
}

func TestSessionRejectsInvalidSession(t *testing.T) {
	mock := &authHandlerServiceMock{sessionErr: ErrInvalidSession}
	h := NewHandler(mock, nil)
	app := buildAuthHandlerSessionTestApp(h, 0)

	req := httptest.NewRequest(http.MethodGet, "/session", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("request failed: %v", err)
	}
	if resp.StatusCode != fiber.StatusUnauthorized {
		t.Fatalf("expected %d, got %d", fiber.StatusUnauthorized, resp.StatusCode)
	}
}
