package auth

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"
)

// Application scope (authz audit 2026-08-25): having a backend account must
// NOT grant entry to every frontend. Verify derivation + enforcement.

func TestDeriveApplicationAccess(t *testing.T) {
	cases := []struct {
		role    Role
		ecomAdm bool
		want    ApplicationAccess
	}{
		{RoleAdmin, false, AppPlatform},
		{RoleOwner, false, AppPlatform},
		{RoleBuyer, false, AppECommerce},
		{RoleBuyer, true, AppECommerceAdmin}, // flagged buyer = B2B backoffice identity
		{RoleSales, false, AppERP},           // internal operator = ERP
		{RoleFinance, false, AppERP},
		{RoleWarehouse, false, AppERP},
		{RoleHRManager, false, AppERP},
		{RoleHRStaff, false, AppERP},
		{RoleEmployee, false, AppERP},
		{RoleSystem, false, AppERP},
		{RoleSales, true, AppECommerceAdmin}, // B2B backoffice via customer flag only
		{RoleSystem, true, AppECommerceAdmin},
	}
	for _, c := range cases {
		if got := DeriveApplicationAccess(c.role, c.ecomAdm); got != c.want {
			t.Errorf("DeriveApplicationAccess(%q,%v) = %q, want %q", c.role, c.ecomAdm, got, c.want)
		}
	}
}

func TestCanAccessApp(t *testing.T) {
	platform := &User{Role: RoleAdmin, ApplicationAccess: AppPlatform}
	if !platform.CanAccessApp(AppERP) ||
		!platform.CanAccessApp(AppPOS) ||
		!platform.CanAccessApp(AppECommerce) ||
		!platform.CanAccessApp(AppECommerceAdmin) {
		t.Fatal("platform must reach every application")
	}

	erp := &User{Role: RoleSales, ApplicationAccess: AppERP}
	if !erp.CanAccessApp(AppERP) {
		t.Fatal("erp may enter erp")
	}
	if erp.CanAccessApp(AppECommerce) || erp.CanAccessApp(AppECommerceAdmin) {
		t.Fatal("erp must NOT enter e-commerce or its admin")
	}

	buyer := &User{Role: RoleBuyer, ApplicationAccess: AppECommerce}
	if !buyer.CanAccessApp(AppECommerce) {
		t.Fatal("buyer may enter ecommerce storefront")
	}
	if buyer.CanAccessApp(AppERP) || buyer.CanAccessApp(AppPOS) || buyer.CanAccessApp(AppECommerceAdmin) {
		t.Fatal("buyer must NOT enter erp, pos, or ecommerce admin")
	}

	admin := &User{Role: RoleSales, ApplicationAccess: AppECommerceAdmin}
	if !admin.CanAccessApp(AppECommerceAdmin) || !admin.CanAccessApp(AppECommerce) {
		t.Fatal("ecommerce admin may manage the storefront")
	}
	if admin.CanAccessApp(AppERP) || admin.CanAccessApp(AppPOS) {
		t.Fatal("ecommerce admin must NOT enter erp or pos")
	}

	if (&User{}).CanAccessApp(AppERP) {
		t.Fatal("nil-derived user must not access anything")
	}
}

func TestAppScopes(t *testing.T) {
	platform := &User{Role: RoleOwner, ApplicationAccess: AppPlatform}
	if len(platform.AppScopes()) != 4 {
		t.Fatalf("platform exposes 4 scopes, got %d", len(platform.AppScopes()))
	}
	buyer := &User{Role: RoleBuyer, ApplicationAccess: AppECommerce}
	if len(buyer.AppScopes()) != 1 || buyer.AppScopes()[0] != AppECommerce {
		t.Fatalf("buyer exposes ecommerce only, got %v", buyer.AppScopes())
	}
}

func TestRequireAppMiddlewareDeniesCrossApp(t *testing.T) {
	ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })

	// ERP-only user hitting an e-commerce-admin-guarded handler → 403.
	erp := &User{Role: RoleSales, ApplicationAccess: AppERP}
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ToContext(reqCtx(), erp, 1))
	RequireApp(AppECommerceAdmin)(ok).ServeHTTP(rec, req)
	if rec.Code != 403 {
		t.Fatalf("erp user must be 403 on ecommerce_admin route, got %d", rec.Code)
	}

	// E-commerce-admin user hitting an ERP-guarded handler → 403.
	adm := &User{Role: RoleSales, ApplicationAccess: AppECommerceAdmin}
	rec = httptest.NewRecorder()
	req = httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ToContext(reqCtx(), adm, 2))
	RequireApp(AppERP)(ok).ServeHTTP(rec, req)
	if rec.Code != 403 {
		t.Fatalf("ecommerce admin must be 403 on erp route, got %d", rec.Code)
	}

	// Buyer hitting ERP route → 403.
	buyer := &User{Role: RoleBuyer, ApplicationAccess: AppECommerce}
	rec = httptest.NewRecorder()
	req = httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ToContext(reqCtx(), buyer, 3))
	RequireApp(AppERP)(ok).ServeHTTP(rec, req)
	if rec.Code != 403 {
		t.Fatalf("buyer must be 403 on erp route, got %d", rec.Code)
	}

	// Platform (admin) reaches ecommerce_admin → 200.
	plat := &User{Role: RoleAdmin, ApplicationAccess: AppPlatform}
	rec = httptest.NewRecorder()
	req = httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ToContext(reqCtx(), plat, 4))
	RequireApp(AppECommerceAdmin)(ok).ServeHTTP(rec, req)
	if rec.Code != 200 {
		t.Fatalf("platform must reach ecommerce_admin, got %d", rec.Code)
	}

	// Unauthenticated request → 401.
	rec = httptest.NewRecorder()
	req = httptest.NewRequest(http.MethodGet, "/", nil)
	RequireApp(AppERP)(ok).ServeHTTP(rec, req)
	if rec.Code != 401 {
		t.Fatalf("no user must be 401, got %d", rec.Code)
	}
}

func TestRequireAnyAppMiddleware(t *testing.T) {
	ok := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
	checks := []struct {
		name string
		user *User
		want int
	}{
		{"ecommerce buyer allowed", &User{ApplicationAccess: AppECommerce}, http.StatusOK},
		{"erp operator allowed", &User{ApplicationAccess: AppERP}, http.StatusOK},
		{"pos operator denied", &User{ApplicationAccess: AppPOS}, http.StatusForbidden},
		{"platform allowed", &User{ApplicationAccess: AppPlatform}, http.StatusOK},
	}
	for _, tc := range checks {
		t.Run(tc.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ToContext(reqCtx(), tc.user, 1))
			rec := httptest.NewRecorder()
			RequireAnyApp(AppECommerce, AppERP)(ok).ServeHTTP(rec, req)
			if rec.Code != tc.want {
				t.Fatalf("got status %d, want %d", rec.Code, tc.want)
			}
		})
	}
}

// reqCtx returns a fresh background context (ToContext needs a context.Context).
func reqCtx() context.Context { return context.Background() }

func TestHandlerLoginUnverifiedBuyerTargetingERP(t *testing.T) {
	// An unverified e-commerce buyer (email_verified_at == nil) attempting to log into ERP
	// must receive 403 FORBIDDEN with message "Akun ini tidak memiliki hak akses ke aplikasi ERP. Hubungi administrator."
	// rather than 403 EMAIL_NOT_VERIFIED ("Email belum diverifikasi").
	f := newFake(fakeHashUser(10, "buyer@ecommerce.test", RoleBuyer, true))
	f.users["buyer@ecommerce.test"].ApplicationAccess = AppECommerce
	f.users["buyer@ecommerce.test"].EmailVerifiedAt = nil

	svc := NewService(f, time.UTC, 0)
	h := NewHandler(svc)

	body := `{"email":"buyer@ecommerce.test","password":"secret123","app":"erp"}`
	req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	h.Login(rec, req)

	if rec.Code != http.StatusForbidden {
		t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
	}

	var resp struct {
		Error struct {
			Code    string `json:"code"`
			Message string `json:"message"`
		} `json:"error"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("json decode err: %v", err)
	}

	if resp.Error.Code != "FORBIDDEN" {
		t.Errorf("code = %q, want FORBIDDEN", resp.Error.Code)
	}
	wantMsg := "Akun ini tidak memiliki hak akses ke aplikasi ERP. Hubungi administrator."
	if resp.Error.Message != wantMsg {
		t.Errorf("message = %q, want %q", resp.Error.Message, wantMsg)
	}

	// Also test targeting POS:
	bodyPOS := `{"email":"buyer@ecommerce.test","password":"secret123","app":"pos"}`
	reqPOS := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(bodyPOS))
	reqPOS.Header.Set("Content-Type", "application/json")
	recPOS := httptest.NewRecorder()

	h.Login(recPOS, reqPOS)

	if recPOS.Code != http.StatusForbidden {
		t.Fatalf("pos status = %d, want %d", recPOS.Code, http.StatusForbidden)
	}
	var respPOS struct {
		Error struct {
			Code    string `json:"code"`
			Message string `json:"message"`
		} `json:"error"`
	}
	if err := json.Unmarshal(recPOS.Body.Bytes(), &respPOS); err != nil {
		t.Fatalf("json decode err: %v", err)
	}
	if respPOS.Error.Code != "FORBIDDEN" {
		t.Errorf("pos code = %q, want FORBIDDEN", respPOS.Error.Code)
	}
	wantPOSMsg := "Akun ini tidak memiliki hak akses ke aplikasi POS. Hubungi administrator."
	if respPOS.Error.Message != wantPOSMsg {
		t.Errorf("pos message = %q, want %q", respPOS.Error.Message, wantPOSMsg)
	}

	// Targeting ecommerce still gives EMAIL_NOT_VERIFIED:
	bodyEcom := `{"email":"buyer@ecommerce.test","password":"secret123","app":"ecommerce"}`
	reqEcom := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", strings.NewReader(bodyEcom))
	reqEcom.Header.Set("Content-Type", "application/json")
	recEcom := httptest.NewRecorder()

	h.Login(recEcom, reqEcom)

	if recEcom.Code != http.StatusForbidden {
		t.Fatalf("ecom status = %d, want %d", recEcom.Code, http.StatusForbidden)
	}
	var respEcom struct {
		Error struct {
			Code    string `json:"code"`
			Message string `json:"message"`
		} `json:"error"`
	}
	if err := json.Unmarshal(recEcom.Body.Bytes(), &respEcom); err != nil {
		t.Fatalf("json decode err: %v", err)
	}
	if respEcom.Error.Code != "EMAIL_NOT_VERIFIED" {
		t.Errorf("ecom code = %q, want EMAIL_NOT_VERIFIED", respEcom.Error.Code)
	}
}
