package middleware

import (
	"context"
	"io"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"

	"wucher/internal/config"
	"wucher/internal/domain/auth"
	"wucher/internal/service"
	sharedconst "wucher/internal/shared/const"
	"wucher/internal/shared/pkg/appctx"
	"wucher/internal/shared/pkg/uuidv7"
)

type middlewareAuthRepo struct {
	users      map[string]*auth.User
	getByIDErr error
}

func newMiddlewareAuthRepo() *middlewareAuthRepo {
	return &middlewareAuthRepo{users: map[string]*auth.User{}}
}

func (r *middlewareAuthRepo) CreateUser(_ context.Context, user *auth.User) error {
	if user != nil && !user.IsActive {
		user.IsActive = true
	}
	r.users[string(user.ID)] = user
	return nil
}
func (r *middlewareAuthRepo) GetUserByID(_ context.Context, id []byte) (*auth.User, error) {
	if r.getByIDErr != nil {
		return nil, r.getByIDErr
	}
	return r.users[string(id)], nil
}
func (r *middlewareAuthRepo) GetUserByEmail(_ context.Context, _ string) (*auth.User, error) {
	return nil, nil
}
func (r *middlewareAuthRepo) GetUserBySSOEmail(_ context.Context, _ string) (*auth.User, error) {
	return nil, nil
}
func (r *middlewareAuthRepo) GetUserByUsername(_ context.Context, _ string) (*auth.User, error) {
	return nil, nil
}
func (r *middlewareAuthRepo) UpdateUserTimezone(_ context.Context, _ []byte, _ string) error {
	return nil
}
func (r *middlewareAuthRepo) SetUserPassword(_ context.Context, _ []byte, _ []byte, _ []byte) error {
	return nil
}
func (r *middlewareAuthRepo) SetUserSecurityPIN(_ context.Context, id []byte, pinHash []byte, setAt time.Time) error {
	u := r.users[string(id)]
	if u == nil {
		return nil
	}
	u.SecurityPINHash = append([]byte(nil), pinHash...)
	ts := setAt.UTC()
	u.SecurityPINSetAt = &ts
	return nil
}
func (r *middlewareAuthRepo) MarkEmailVerified(_ context.Context, _ []byte) error {
	return nil
}
func (r *middlewareAuthRepo) CreatePasswordResetToken(_ context.Context, _ *auth.PasswordResetToken) error {
	return nil
}
func (r *middlewareAuthRepo) InvalidateActivePasswordResetTokensByUserID(_ context.Context, _ []byte, _ time.Time) error {
	return nil
}
func (r *middlewareAuthRepo) ConsumePasswordResetTokenAndSetPassword(_ context.Context, _ []byte, _ []byte, _ []byte, _ time.Time) ([]byte, bool, error) {
	return nil, false, nil
}
func (r *middlewareAuthRepo) CreateSecurityPINResetToken(_ context.Context, _ *auth.SecurityPINResetToken) error {
	return nil
}
func (r *middlewareAuthRepo) InvalidateActiveSecurityPINResetTokensByUserID(_ context.Context, _ []byte, _ time.Time) error {
	return nil
}
func (r *middlewareAuthRepo) GetActiveSecurityPINResetTokenUser(_ context.Context, _ []byte, _ time.Time) (*auth.User, error) {
	return nil, nil
}
func (r *middlewareAuthRepo) ConsumeSecurityPINResetTokenAndSetPIN(_ context.Context, _ []byte, _ []byte, _ time.Time) ([]byte, bool, error) {
	return nil, false, nil
}
func (r *middlewareAuthRepo) UpsertIdentity(_ context.Context, _ *auth.UserIdentity) error {
	return nil
}
func (r *middlewareAuthRepo) CreateIdentity(_ context.Context, _ *auth.UserIdentity) error {
	return nil
}
func (r *middlewareAuthRepo) GetIdentityByProviderSubject(_ context.Context, _ string, _ string) (*auth.UserIdentity, error) {
	return nil, nil
}
func (r *middlewareAuthRepo) GetIdentityByUserID(_ context.Context, _ []byte) (*auth.UserIdentity, error) {
	return nil, nil
}
func (r *middlewareAuthRepo) DeleteIdentityByUserIDProvider(_ context.Context, _ []byte, _ string) (bool, error) {
	return false, nil
}
func (r *middlewareAuthRepo) GetUserTOTP(_ context.Context, _ []byte) (*auth.UserTOTP, error) {
	return nil, nil
}
func (r *middlewareAuthRepo) UpsertUserTOTP(_ context.Context, _ *auth.UserTOTP) error {
	return nil
}
func (r *middlewareAuthRepo) DisableUserTOTP(_ context.Context, _ []byte) error {
	return nil
}
func (r *middlewareAuthRepo) DeleteUserTOTPSetup(_ context.Context, _ []byte) error {
	return nil
}
func (r *middlewareAuthRepo) UpdateUserTOTPLastUsed(_ context.Context, _ []byte) error {
	return nil
}
func (r *middlewareAuthRepo) GetActiveUserEmailLoginOTP(_ context.Context, _ []byte, _ time.Time) (*auth.UserEmailLoginOTP, error) {
	return nil, nil
}
func (r *middlewareAuthRepo) CountUserEmailLoginOTPSince(_ context.Context, _ []byte, _ time.Time) (int64, error) {
	return 0, nil
}
func (r *middlewareAuthRepo) CreateUserEmailLoginOTP(_ context.Context, _ *auth.UserEmailLoginOTP) error {
	return nil
}
func (r *middlewareAuthRepo) InvalidateActiveUserEmailLoginOTPs(_ context.Context, _ []byte, _ time.Time) error {
	return nil
}
func (r *middlewareAuthRepo) ConsumeUserEmailLoginOTP(_ context.Context, _ []byte, _ []byte, _ time.Time) (bool, int, error) {
	return false, 0, nil
}
func (r *middlewareAuthRepo) ListUserWebAuthnCredentials(_ context.Context, _ []byte) ([]auth.UserWebAuthnCredential, error) {
	return nil, nil
}
func (r *middlewareAuthRepo) CreateUserWebAuthnCredential(_ context.Context, _ *auth.UserWebAuthnCredential) error {
	return nil
}
func (r *middlewareAuthRepo) UpdateUserWebAuthnCredential(_ context.Context, _ []byte, _ []byte, _ []byte, _ time.Time) error {
	return nil
}
func (r *middlewareAuthRepo) DeleteAllUserWebAuthnCredentials(_ context.Context, _ []byte) error {
	return nil
}

type middlewareRoleRepo struct {
	allowed            bool
	err                error
	permissionByKey    *auth.Permission
	permissionByKeyErr error
}

type middlewareTokenStore struct {
	store map[string][]byte
}

func newMiddlewareTokenStore() *middlewareTokenStore {
	return &middlewareTokenStore{store: map[string][]byte{}}
}

func (m *middlewareTokenStore) Set(_ context.Context, key string, value []byte, _ time.Duration) error {
	m.store[key] = append([]byte(nil), value...)
	return nil
}

func (m *middlewareTokenStore) SetIfNotExists(_ context.Context, key string, value []byte, _ time.Duration) (bool, error) {
	if _, ok := m.store[key]; ok {
		return false, nil
	}
	m.store[key] = append([]byte(nil), value...)
	return true, nil
}

func (m *middlewareTokenStore) Get(_ context.Context, key string) ([]byte, error) {
	v, ok := m.store[key]
	if !ok {
		return nil, nil
	}
	return append([]byte(nil), v...), nil
}

func (m *middlewareTokenStore) Delete(_ context.Context, key string) error {
	delete(m.store, key)
	return nil
}

func (r *middlewareRoleRepo) CreateRole(_ context.Context, _ *auth.Role) error { return nil }
func (r *middlewareRoleRepo) UpdateRole(_ context.Context, _ *auth.Role) error { return nil }
func (r *middlewareRoleRepo) DeleteRole(_ context.Context, _ []byte) error     { return nil }
func (r *middlewareRoleRepo) GetRoleByID(_ context.Context, _ []byte) (*auth.Role, error) {
	return nil, nil
}
func (r *middlewareRoleRepo) GetRoleByName(_ context.Context, _ string) (*auth.Role, error) {
	return nil, nil
}
func (r *middlewareRoleRepo) HasRolePermission(_ context.Context, _ []byte, _ string) (bool, error) {
	if r.err != nil {
		return false, r.err
	}
	return r.allowed, nil
}
func (r *middlewareRoleRepo) GetPermissionByID(_ context.Context, _ []byte) (*auth.Permission, error) {
	return nil, nil
}
func (r *middlewareRoleRepo) GetPermissionByKey(_ context.Context, _ string) (*auth.Permission, error) {
	return r.permissionByKey, r.permissionByKeyErr
}
func (r *middlewareRoleRepo) UpdatePermission(_ context.Context, _ *auth.Permission) error {
	return nil
}
func (r *middlewareRoleRepo) ListPermissionsByRoleID(_ context.Context, _ []byte) ([]auth.Permission, error) {
	return nil, nil
}
func (r *middlewareRoleRepo) ListPermissions(_ context.Context, _ string, _ string, _ int, _ int) ([]auth.Permission, int64, error) {
	return nil, 0, nil
}
func (r *middlewareRoleRepo) AssignPermission(_ context.Context, roleID, permissionID []byte) (*auth.RolePermission, error) {
	return &auth.RolePermission{RoleID: roleID, PermissionID: permissionID}, nil
}
func (r *middlewareRoleRepo) RemovePermission(_ context.Context, _ []byte, _ []byte) error {
	return nil
}
func (r *middlewareRoleRepo) ListRoles(_ context.Context, _ string, _ string, _ int, _ int) ([]auth.Role, int64, error) {
	return nil, 0, nil
}

func TestAuthRequired(t *testing.T) {
	t.Run("service not configured", func(t *testing.T) {
		app := fiber.New()
		app.Use(AuthRequired(nil))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid access token", func(t *testing.T) {
		svc := service.NewAuthService(nil, nil, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
			JWTAccessSecret:     "access-secret",
			JWTRefreshSecret:    "refresh-secret",
			JWTAccessTTL:        time.Minute,
			JWTRefreshTTL:       time.Hour,
			JWTAccessCookieName: "at",
		}})

		app := fiber.New()
		app.Use(AuthRequired(svc))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		req.AddCookie(&http.Cookie{Name: svc.AccessCookieName(), Value: "not-a-jwt"})
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", resp.StatusCode)
		}
	})

	t.Run("success stores user_id and continues", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		tokens := newMiddlewareTokenStore()
		svc := service.NewAuthService(repo, nil, tokens, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
			JWTAccessSecret:     "access-secret",
			JWTRefreshSecret:    "refresh-secret",
			JWTAccessTTL:        time.Minute,
			JWTRefreshTTL:       time.Hour,
			JWTAccessCookieName: "at",
		}})
		userID := uuidv7.MustBytes()
		user := &auth.User{ID: userID, IsActive: true}
		if err := repo.CreateUser(context.Background(), user); err != nil {
			t.Fatalf("create user: %v", err)
		}
		pair, err := svc.IssueTokensWithClientForProvider(context.Background(), user, "", "", "microsoft")
		if err != nil {
			t.Fatalf("issue tokens: %v", err)
		}

		app := fiber.New()
		app.Use(AuthRequired(svc))
		app.Get("/protected", func(c *fiber.Ctx) error {
			raw := c.Locals("user_id")
			got, ok := raw.([]byte)
			if !ok || string(got) != string(userID) {
				return c.Status(http.StatusInternalServerError).SendString("missing user context")
			}
			if got := appctx.GetUserID(c.UserContext()); string(got) != string(userID) {
				return c.Status(http.StatusInternalServerError).SendString("missing user id in request context")
			}
			return c.SendStatus(http.StatusOK)
		})

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		req.AddCookie(&http.Cookie{Name: svc.AccessCookieName(), Value: pair.AccessToken})
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			body, _ := io.ReadAll(resp.Body)
			t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(body))
		}
	})

	t.Run("duplicate access cookies use the valid token", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		tokens := newMiddlewareTokenStore()
		svc := service.NewAuthService(repo, nil, tokens, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
			JWTAccessSecret:     "access-secret",
			JWTRefreshSecret:    "refresh-secret",
			JWTAccessTTL:        time.Minute,
			JWTRefreshTTL:       time.Hour,
			JWTAccessCookieName: "at",
		}})
		userID := uuidv7.MustBytes()
		user := &auth.User{ID: userID, IsActive: true}
		if err := repo.CreateUser(context.Background(), user); err != nil {
			t.Fatalf("create user: %v", err)
		}
		pair, err := svc.IssueTokensWithClientForProvider(context.Background(), user, "", "", "microsoft")
		if err != nil {
			t.Fatalf("issue tokens: %v", err)
		}

		app := fiber.New()
		app.Use(AuthRequired(svc))
		app.Get("/protected", func(c *fiber.Ctx) error {
			raw := c.Locals("user_id")
			got, ok := raw.([]byte)
			if !ok || string(got) != string(userID) {
				return c.Status(http.StatusInternalServerError).SendString("missing user context")
			}
			return c.SendStatus(http.StatusOK)
		})

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		req.Header.Set("Cookie", svc.AccessCookieName()+"=not-a-valid-token; "+svc.AccessCookieName()+"="+pair.AccessToken)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			body, _ := io.ReadAll(resp.Body)
			t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(body))
		}
	})

	t.Run("expired by idle timeout", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		tokens := newMiddlewareTokenStore()
		svc := service.NewAuthService(repo, nil, tokens, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{
			JWTAccessSecret:     "access-secret",
			JWTRefreshSecret:    "refresh-secret",
			JWTAccessTTL:        time.Minute,
			JWTRefreshTTL:       time.Hour,
			SessionIdleTTL:      time.Millisecond,
			JWTAccessCookieName: "at",
		}})
		userID := uuidv7.MustBytes()
		user := &auth.User{ID: userID, IsActive: true}
		if err := repo.CreateUser(context.Background(), user); err != nil {
			t.Fatalf("create user: %v", err)
		}
		pair, err := svc.IssueTokensWithClientForProvider(context.Background(), user, "", "", "microsoft")
		if err != nil {
			t.Fatalf("issue tokens: %v", err)
		}
		time.Sleep(5 * time.Millisecond)

		app := fiber.New()
		app.Use(AuthRequired(svc))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		req.AddCookie(&http.Cookie{Name: svc.AccessCookieName(), Value: pair.AccessToken})
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401 for idle-expired session, got %d", resp.StatusCode)
		}
	})
}

func TestPermissionRequired(t *testing.T) {
	t.Run("service not configured", func(t *testing.T) {
		app := fiber.New()
		app.Use(PermissionRequired(nil, "users.read"))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", resp.StatusCode)
		}
	})

	t.Run("missing auth context", func(t *testing.T) {
		svc := service.NewAuthService(nil, nil, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		app := fiber.New()
		app.Use(PermissionRequired(svc, "users.read"))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", resp.StatusCode)
		}
	})

	t.Run("permission check error", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		roleRepo := &middlewareRoleRepo{err: context.DeadlineExceeded}
		svc := service.NewAuthService(repo, roleRepo, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})

		userID := uuidv7.MustBytes()
		roleID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, RoleID: roleID}

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PermissionRequired(svc, "users.read"))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusForbidden {
			t.Fatalf("expected 403, got %d", resp.StatusCode)
		}
	})

	t.Run("insufficient permission", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		roleRepo := &middlewareRoleRepo{allowed: false}
		svc := service.NewAuthService(repo, roleRepo, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})

		userID := uuidv7.MustBytes()
		roleID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, RoleID: roleID}

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PermissionRequired(svc, "users.read"))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusForbidden {
			t.Fatalf("expected 403, got %d", resp.StatusCode)
		}
		body, _ := io.ReadAll(resp.Body)
		if !strings.Contains(string(body), "insufficient permission") {
			t.Fatalf("unexpected body: %s", string(body))
		}
	})

	t.Run("allowed permission continues", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		roleRepo := &middlewareRoleRepo{allowed: true}
		svc := service.NewAuthService(repo, roleRepo, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})

		userID := uuidv7.MustBytes()
		roleID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, RoleID: roleID}

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PermissionRequired(svc, "users.read"))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

	t.Run("requires pin without token returns 202", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		roleRepo := &middlewareRoleRepo{
			allowed:         true,
			permissionByKey: &auth.Permission{RequiresPIN: true},
		}
		tokens := newMiddlewareTokenStore()
		svc := service.NewAuthService(
			repo,
			roleRepo,
			tokens,
			nil,
			service.AuthServiceDependencies{
				SSO: nil, Protector: nil, Config: config.AuthConfig{
					JWTAccessSecret:           "access-secret",
					JWTRefreshSecret:          "refresh-secret",
					JWTAccessTTL:              time.Minute,
					JWTRefreshTTL:             time.Minute * 10,
					SecurityPINActionTokenTTL: time.Minute,
				},
			},
		)

		userID := uuidv7.MustBytes()
		roleID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, RoleID: roleID}

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PermissionRequired(svc, "users.read"))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusAccepted {
			t.Fatalf("expected 202, got %d", resp.StatusCode)
		}
	})

	t.Run("requires pin with mapped action token returns 200", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		roleRepo := &middlewareRoleRepo{
			allowed:         true,
			permissionByKey: &auth.Permission{RequiresPIN: true},
		}
		tokens := newMiddlewareTokenStore()
		svc := service.NewAuthService(
			repo,
			roleRepo,
			tokens,
			nil,
			service.AuthServiceDependencies{
				SSO: nil, Protector: nil, Config: config.AuthConfig{
					JWTAccessSecret:           "access-secret",
					JWTRefreshSecret:          "refresh-secret",
					JWTAccessTTL:              time.Minute,
					JWTRefreshTTL:             time.Minute * 10,
					SecurityPINActionTokenTTL: time.Minute,
				},
			},
		)

		userID := uuidv7.MustBytes()
		roleID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, RoleID: roleID}
		if err := svc.SetSecurityPIN(context.Background(), userID, "123456"); err != nil {
			t.Fatalf("SetSecurityPIN: %v", err)
		}

		token, err := svc.VerifySecurityPINForAction(context.Background(), userID, "123456", sharedconst.PinActionBaseCreate)
		if err != nil {
			t.Fatalf("VerifySecurityPINForAction: %v", err)
		}

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PermissionRequired(svc, "base.create"))
		app.Post("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		req.Header.Set(permissionPinVerificationHeader, token)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

	t.Run("reusable variant accepts the same token twice", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		roleRepo := &middlewareRoleRepo{
			allowed:         true,
			permissionByKey: &auth.Permission{RequiresPIN: true},
		}
		tokens := newMiddlewareTokenStore()
		svc := service.NewAuthService(
			repo,
			roleRepo,
			tokens,
			nil,
			service.AuthServiceDependencies{
				SSO: nil, Protector: nil, Config: config.AuthConfig{
					JWTAccessSecret:           "access-secret",
					JWTRefreshSecret:          "refresh-secret",
					JWTAccessTTL:              time.Minute,
					JWTRefreshTTL:             time.Minute * 10,
					SecurityPINActionTokenTTL: time.Minute,
				},
			},
		)

		userID := uuidv7.MustBytes()
		roleID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, RoleID: roleID}
		if err := svc.SetSecurityPIN(context.Background(), userID, "123456"); err != nil {
			t.Fatalf("SetSecurityPIN: %v", err)
		}

		token, err := svc.VerifySecurityPINForAction(context.Background(), userID, "123456", sharedconst.PinActionAuditLogRead)
		if err != nil {
			t.Fatalf("VerifySecurityPINForAction: %v", err)
		}

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PermissionRequiredReusable(svc, "audit.read"))
		app.Get("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req1, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		req1.Header.Set(permissionPinVerificationHeader, token)
		resp1, err := app.Test(req1)
		if err != nil {
			t.Fatalf("app.Test first: %v", err)
		}
		if resp1.StatusCode != http.StatusOK {
			t.Fatalf("expected first request 200, got %d", resp1.StatusCode)
		}

		req2, _ := http.NewRequest(http.MethodGet, "/protected", nil)
		req2.Header.Set(permissionPinVerificationHeader, token)
		resp2, err := app.Test(req2)
		if err != nil {
			t.Fatalf("app.Test second: %v", err)
		}
		if resp2.StatusCode != http.StatusOK {
			t.Fatalf("expected second request 200, got %d", resp2.StatusCode)
		}
	})

	t.Run("consuming variant rejects the same token twice", func(t *testing.T) {
		repo := newMiddlewareAuthRepo()
		roleRepo := &middlewareRoleRepo{
			allowed:         true,
			permissionByKey: &auth.Permission{RequiresPIN: true},
		}
		tokens := newMiddlewareTokenStore()
		svc := service.NewAuthService(
			repo,
			roleRepo,
			tokens,
			nil,
			service.AuthServiceDependencies{
				SSO: nil, Protector: nil, Config: config.AuthConfig{
					JWTAccessSecret:           "access-secret",
					JWTRefreshSecret:          "refresh-secret",
					JWTAccessTTL:              time.Minute,
					JWTRefreshTTL:             time.Minute * 10,
					SecurityPINActionTokenTTL: time.Minute,
				},
			},
		)

		userID := uuidv7.MustBytes()
		roleID := uuidv7.MustBytes()
		repo.users[string(userID)] = &auth.User{ID: userID, RoleID: roleID}
		if err := svc.SetSecurityPIN(context.Background(), userID, "123456"); err != nil {
			t.Fatalf("SetSecurityPIN: %v", err)
		}

		token, err := svc.VerifySecurityPINForAction(context.Background(), userID, "123456", sharedconst.PinActionBaseCreate)
		if err != nil {
			t.Fatalf("VerifySecurityPINForAction: %v", err)
		}

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PermissionRequired(svc, "base.create"))
		app.Post("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req1, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		req1.Header.Set(permissionPinVerificationHeader, token)
		resp1, err := app.Test(req1)
		if err != nil {
			t.Fatalf("app.Test first: %v", err)
		}
		if resp1.StatusCode != http.StatusOK {
			t.Fatalf("expected first request 200, got %d", resp1.StatusCode)
		}

		req2, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		req2.Header.Set(permissionPinVerificationHeader, token)
		resp2, err := app.Test(req2)
		if err != nil {
			t.Fatalf("app.Test second: %v", err)
		}
		if resp2.StatusCode != http.StatusAccepted {
			t.Fatalf("expected second request 202, got %d", resp2.StatusCode)
		}
	})
}

func TestPinRequired(t *testing.T) {
	t.Run("service not configured", func(t *testing.T) {
		app := fiber.New()
		app.Use(PinRequired(nil, "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74"))
		app.Post("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", resp.StatusCode)
		}
	})

	t.Run("missing auth context", func(t *testing.T) {
		svc := service.NewAuthService(nil, nil, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		app := fiber.New()
		app.Use(PinRequired(svc, "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74"))
		app.Post("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusUnauthorized {
			t.Fatalf("expected 401, got %d", resp.StatusCode)
		}
	})

	t.Run("invalid pin action token", func(t *testing.T) {
		tokenStore := newMiddlewareTokenStore()
		svc := service.NewAuthService(nil, nil, tokenStore, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		userID := uuidv7.MustBytes()

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PinRequired(svc, "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74"))
		app.Post("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		req.Header.Set(pinVerificationHeader, "bad-token")
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusAccepted {
			t.Fatalf("expected 202, got %d", resp.StatusCode)
		}
	})

	t.Run("valid pin action token", func(t *testing.T) {
		tokenStore := newMiddlewareTokenStore()
		svc := service.NewAuthService(nil, nil, tokenStore, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		userID := uuidv7.MustBytes()
		userIDStr, err := uuidv7.BytesToString(userID)
		if err != nil {
			t.Fatalf("bytes to string: %v", err)
		}
		action := "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74"
		token := "token-ok"
		key := "auth:pin_action:" + token
		tokenStore.store[key] = []byte(userIDStr + "|" + action)

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PinRequired(svc, action))
		app.Post("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		req.Header.Set(pinVerificationHeader, token)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			body, _ := io.ReadAll(resp.Body)
			t.Fatalf("expected 200, got %d body=%s", resp.StatusCode, string(body))
		}

		req2, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		req2.Header.Set(pinVerificationHeader, token)
		resp2, err := app.Test(req2)
		if err != nil {
			t.Fatalf("app.Test second: %v", err)
		}
		if resp2.StatusCode != http.StatusAccepted {
			t.Fatalf("expected second request 202, got %d", resp2.StatusCode)
		}
	})
}

func TestPinRequiredForPermission(t *testing.T) {
	t.Run("skip when permission does not require pin", func(t *testing.T) {
		roleRepo := &middlewareRoleRepo{permissionByKey: &auth.Permission{RequiresPIN: false}}
		svc := service.NewAuthService(nil, roleRepo, nil, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		userID := uuidv7.MustBytes()

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PinRequiredForPermission(svc, "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74", "user.delete"))
		app.Post("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp.StatusCode)
		}
	})

	t.Run("require pin when permission policy enabled", func(t *testing.T) {
		tokenStore := newMiddlewareTokenStore()
		roleRepo := &middlewareRoleRepo{permissionByKey: &auth.Permission{RequiresPIN: true}}
		svc := service.NewAuthService(nil, roleRepo, tokenStore, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		userID := uuidv7.MustBytes()

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PinRequiredForPermission(svc, "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74", "user.delete"))
		app.Post("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		req, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		resp, err := app.Test(req)
		if err != nil {
			t.Fatalf("app.Test: %v", err)
		}
		if resp.StatusCode != http.StatusAccepted {
			t.Fatalf("expected 202, got %d", resp.StatusCode)
		}

		userIDStr, err := uuidv7.BytesToString(userID)
		if err != nil {
			t.Fatalf("bytes to string: %v", err)
		}
		token := "token-ok"
		tokenStore.store["auth:pin_action:"+token] = []byte(userIDStr + "|a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74")

		req2, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		req2.Header.Set(pinVerificationHeader, token)
		resp2, err := app.Test(req2)
		if err != nil {
			t.Fatalf("app.Test second: %v", err)
		}
		if resp2.StatusCode != http.StatusOK {
			t.Fatalf("expected 200, got %d", resp2.StatusCode)
		}
	})

	t.Run("reusable token when permission policy enabled", func(t *testing.T) {
		tokenStore := newMiddlewareTokenStore()
		roleRepo := &middlewareRoleRepo{permissionByKey: &auth.Permission{RequiresPIN: true}}
		svc := service.NewAuthService(nil, roleRepo, tokenStore, nil, service.AuthServiceDependencies{SSO: nil, Protector: nil, Config: config.AuthConfig{}})
		userID := uuidv7.MustBytes()

		app := fiber.New()
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_id", userID)
			return c.Next()
		})
		app.Use(PinRequiredReusableForPermission(svc, "a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74", "user.delete"))
		app.Post("/protected", func(c *fiber.Ctx) error { return c.SendStatus(http.StatusOK) })

		userIDStr, err := uuidv7.BytesToString(userID)
		if err != nil {
			t.Fatalf("bytes to string: %v", err)
		}
		token := "token-reusable"
		tokenStore.store["auth:pin_action:"+token] = []byte(userIDStr + "|a546de2f-f1e6-4c2c-ab03-9b4bf39f4d74")

		req1, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		req1.Header.Set(pinVerificationHeader, token)
		resp1, err := app.Test(req1)
		if err != nil {
			t.Fatalf("app.Test first: %v", err)
		}
		if resp1.StatusCode != http.StatusOK {
			t.Fatalf("expected first request 200, got %d", resp1.StatusCode)
		}

		req2, _ := http.NewRequest(http.MethodPost, "/protected", nil)
		req2.Header.Set(pinVerificationHeader, token)
		resp2, err := app.Test(req2)
		if err != nil {
			t.Fatalf("app.Test second: %v", err)
		}
		if resp2.StatusCode != http.StatusOK {
			t.Fatalf("expected second request 200, got %d", resp2.StatusCode)
		}
	})
}
