package handlers

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

	"Noooste/garage-ui/internal/auth"
	"Noooste/garage-ui/internal/config"

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

// newAuthTestService builds a real auth.Service with OIDC disabled. The JWT
// key is auto-generated, matching the production default.
func newAuthTestService(t *testing.T, admin config.AdminAuthConfig) *auth.Service {
	t.Helper()
	svc, err := auth.NewAuthService(
		&config.AuthConfig{
			Admin: admin,
			OIDC:  config.OIDCConfig{Enabled: false},
		},
		&config.ServerConfig{},
	)
	if err != nil {
		t.Fatalf("NewAuthService: %v", err)
	}
	return svc
}

// newAuthTestApp builds a bare Fiber app with the auth handler mounted.
// The admin and OIDC config are reflected both in cfg (for the handler) and
// in the auth service.
func newAuthTestApp(t *testing.T, cfg *config.Config) (*fiber.App, *AuthHandler) {
	t.Helper()
	svc := newAuthTestService(t, cfg.Auth.Admin)
	h := NewAuthHandler(cfg, svc)
	app := fiber.New()
	app.Get("/auth/config", h.GetAuthConfig)
	app.Post("/auth/login", h.LoginAdmin)
	app.Get("/auth/me", h.GetMe)
	return app, h
}

func TestGetAuthConfig_AdminOnly(t *testing.T) {
	cfg := &config.Config{
		Auth: config.AuthConfig{
			Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "p"},
			OIDC:  config.OIDCConfig{Enabled: false},
		},
	}
	app, _ := newAuthTestApp(t, cfg)
	req := httptest.NewRequest(http.MethodGet, "/auth/config", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d, want 200", resp.StatusCode)
	}
	var body struct {
		Admin struct {
			Enabled bool `json:"enabled"`
		} `json:"admin"`
		OIDC struct {
			Enabled  bool   `json:"enabled"`
			Provider string `json:"provider"`
		} `json:"oidc"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !body.Admin.Enabled {
		t.Error("admin.enabled = false, want true")
	}
	if body.OIDC.Enabled {
		t.Error("oidc.enabled = true, want false")
	}
	if body.OIDC.Provider != "" {
		t.Errorf("oidc.provider = %q, want empty", body.OIDC.Provider)
	}
}

func TestGetAuthConfig_OIDCOnly_WithExplicitProvider(t *testing.T) {
	cfg := &config.Config{
		Auth: config.AuthConfig{
			Admin: config.AdminAuthConfig{Enabled: false},
			OIDC: config.OIDCConfig{
				Enabled:      false, // service init skipped (newAuthTestService disables OIDC); handler only reads flags
				ProviderName: "Keycloak",
			},
		},
	}
	// Re-enable OIDC only on the cfg the handler sees — the service is still
	// constructed with OIDC disabled above, which is fine because
	// GetAuthConfig does not touch the service at all.
	cfg.Auth.OIDC.Enabled = true
	app, _ := newAuthTestApp(t, cfg)

	req := httptest.NewRequest(http.MethodGet, "/auth/config", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d, want 200", resp.StatusCode)
	}
	var body struct {
		OIDC struct {
			Enabled  bool   `json:"enabled"`
			Provider string `json:"provider"`
		} `json:"oidc"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !body.OIDC.Enabled {
		t.Error("oidc.enabled = false, want true")
	}
	if body.OIDC.Provider != "Keycloak" {
		t.Errorf("oidc.provider = %q, want Keycloak", body.OIDC.Provider)
	}
}

func TestGetAuthConfig_OIDCEnabled_DefaultProviderName(t *testing.T) {
	cfg := &config.Config{
		Auth: config.AuthConfig{
			Admin: config.AdminAuthConfig{Enabled: false},
			OIDC:  config.OIDCConfig{Enabled: true, ProviderName: ""},
		},
	}
	app, _ := newAuthTestApp(t, cfg)
	req := httptest.NewRequest(http.MethodGet, "/auth/config", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	var body struct {
		OIDC struct {
			Provider string `json:"provider"`
		} `json:"oidc"`
	}
	_ = json.NewDecoder(resp.Body).Decode(&body)
	if body.OIDC.Provider != "OIDC Provider" {
		t.Errorf("provider = %q, want default 'OIDC Provider'", body.OIDC.Provider)
	}
}

func TestLoginAdmin_HappyPath(t *testing.T) {
	cfg := &config.Config{
		Auth: config.AuthConfig{
			Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "s3cret"},
		},
	}
	app, _ := newAuthTestApp(t, cfg)

	body, _ := json.Marshal(map[string]string{"username": "admin", "password": "s3cret"})
	req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		raw, _ := io.ReadAll(resp.Body)
		t.Fatalf("status = %d, want 200\nbody: %s", resp.StatusCode, raw)
	}

	var decoded struct {
		Success bool   `json:"success"`
		Token   string `json:"token"`
		User    struct {
			Username string `json:"username"`
		} `json:"user"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if !decoded.Success {
		t.Error("success = false")
	}
	if decoded.Token == "" {
		t.Error("token empty")
	}
	if decoded.User.Username != "admin" {
		t.Errorf("username = %q, want admin", decoded.User.Username)
	}
}

func TestLoginAdmin_WrongPasswordReturns401(t *testing.T) {
	cfg := &config.Config{
		Auth: config.AuthConfig{Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "s3cret"}},
	}
	app, _ := newAuthTestApp(t, cfg)
	body, _ := json.Marshal(map[string]string{"username": "admin", "password": "WRONG"})
	req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusUnauthorized {
		t.Fatalf("status = %d, want 401", resp.StatusCode)
	}
}

func TestLoginAdmin_WrongUsernameReturns401(t *testing.T) {
	cfg := &config.Config{
		Auth: config.AuthConfig{Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "s3cret"}},
	}
	app, _ := newAuthTestApp(t, cfg)
	body, _ := json.Marshal(map[string]string{"username": "root", "password": "s3cret"})
	req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusUnauthorized {
		t.Fatalf("status = %d, want 401", resp.StatusCode)
	}
}

func TestLoginAdmin_MalformedJSONReturns400(t *testing.T) {
	cfg := &config.Config{
		Auth: config.AuthConfig{Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "p"}},
	}
	app, _ := newAuthTestApp(t, cfg)
	req := httptest.NewRequest(http.MethodPost, "/auth/login", strings.NewReader("{not-json"))
	req.Header.Set("Content-Type", "application/json")
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", resp.StatusCode)
	}
}

func TestGetMe_OIDCUserInfoLocal(t *testing.T) {
	cfg := &config.Config{Auth: config.AuthConfig{}}
	app, h := newAuthTestApp(t, cfg)
	// Re-register /auth/me with a pre-handler that seeds c.Locals("userInfo").
	// The default registration in newAuthTestApp lacks Locals; we mount a
	// second path that does.
	app.Get("/me-oidc", func(c fiber.Ctx) error {
		c.Locals("userInfo", &auth.UserInfo{
			Username: "alice",
			Email:    "alice@example.com",
			Name:     "Alice Example",
		})
		return h.GetMe(c)
	})

	req := httptest.NewRequest(http.MethodGet, "/me-oidc", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d, want 200", resp.StatusCode)
	}
	var decoded struct {
		Success bool `json:"success"`
		User    struct {
			Username string `json:"username"`
			Email    string `json:"email"`
			Name     string `json:"name"`
		} `json:"user"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
		t.Fatalf("decode: %v", err)
	}
	if decoded.User.Username != "alice" || decoded.User.Email != "alice@example.com" || decoded.User.Name != "Alice Example" {
		t.Errorf("got %+v", decoded.User)
	}
}

func TestGetMe_BasicAuthUsernameLocal(t *testing.T) {
	cfg := &config.Config{Auth: config.AuthConfig{}}
	app, h := newAuthTestApp(t, cfg)
	app.Get("/me-basic", func(c fiber.Ctx) error {
		c.Locals("username", "admin")
		return h.GetMe(c)
	})
	req := httptest.NewRequest(http.MethodGet, "/me-basic", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("status = %d, want 200", resp.StatusCode)
	}
	var decoded struct {
		User struct {
			Username string `json:"username"`
		} `json:"user"`
	}
	_ = json.NewDecoder(resp.Body).Decode(&decoded)
	if decoded.User.Username != "admin" {
		t.Errorf("username = %q, want admin", decoded.User.Username)
	}
}

func TestGetMe_NoLocalsReturns401(t *testing.T) {
	cfg := &config.Config{Auth: config.AuthConfig{}}
	app, _ := newAuthTestApp(t, cfg)
	req := httptest.NewRequest(http.MethodGet, "/auth/me", nil)
	resp, err := app.Test(req)
	if err != nil {
		t.Fatalf("app.Test: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusUnauthorized {
		t.Fatalf("status = %d, want 401", resp.StatusCode)
	}
}
