package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/service"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/stretchr/testify/require"
)

type fakeRPUsageQueryRepo struct {
	query domain.RPUsageQuery
	items []domain.RPUsageDailyMetric
}

func (f *fakeRPUsageQueryRepo) FindRPUsage(ctx context.Context, query domain.RPUsageQuery) ([]domain.RPUsageDailyMetric, error) {
	f.query = query
	return f.items, nil
}

type fakeAdminKeto struct {
	allowed  bool
	subject  string
	object   string
	relation string
}

func (f *fakeAdminKeto) CheckPermission(ctx context.Context, subject, namespace, object, relation string) (bool, error) {
	f.subject = subject
	f.object = object
	f.relation = relation
	return f.allowed, nil
}

func (f *fakeAdminKeto) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
	return nil
}

func (f *fakeAdminKeto) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
	return nil
}

func (f *fakeAdminKeto) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) {
	return nil, nil
}

func (f *fakeAdminKeto) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
	return nil, nil
}

type fakeOverviewAuditRepo struct {
	mockAuditRepo
	since time.Time
	count int64
}

func (f *fakeOverviewAuditRepo) CountEventsSince(ctx context.Context, since time.Time) (int64, error) {
	f.since = since
	return f.count, nil
}

type fakeAdminUserProjectionRepo struct {
	status domain.UserProjectionStatus
}

func (f *fakeAdminUserProjectionRepo) IsReady(ctx context.Context) (bool, error) {
	return f.status.Ready, nil
}

func (f *fakeAdminUserProjectionRepo) CountTenantMembers(ctx context.Context, tenants []domain.Tenant) (map[string]int64, error) {
	return nil, nil
}

func (f *fakeAdminUserProjectionRepo) ReplaceAllFromKratos(ctx context.Context, users []domain.User) error {
	return nil
}

func (f *fakeAdminUserProjectionRepo) MarkFailed(ctx context.Context, syncErr error) error {
	return nil
}

func (f *fakeAdminUserProjectionRepo) GetStatus(ctx context.Context) (domain.UserProjectionStatus, error) {
	return f.status, nil
}

type fakeAdminUserProjectionSyncer struct {
	count int
	err   error
	calls int
}

func (f *fakeAdminUserProjectionSyncer) Reconcile(ctx context.Context) (int, error) {
	f.calls++
	return f.count, f.err
}

func TestAdminHandler_GetRPUsageDaily(t *testing.T) {
	repo := &fakeRPUsageQueryRepo{
		items: []domain.RPUsageDailyMetric{
			{
				Date:           "2026-05-06",
				TenantID:       "tenant-1",
				TenantType:     domain.TenantTypeCompany,
				ClientID:       "orgfront",
				ClientName:     "OrgFront",
				LoginRequests:  12,
				OtherRequests:  4,
				UniqueSubjects: 8,
			},
		},
	}
	h := &AdminHandler{RPUsageQueries: repo}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Get("/api/v1/admin/rp-usage/daily", h.GetRPUsageDaily)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/rp-usage/daily?days=7&period=week&tenantId=tenant-1", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Equal(t, 7, repo.query.Days)
	require.Equal(t, "week", repo.query.Period)
	require.Equal(t, "tenant-1", repo.query.TenantID)

	var body struct {
		Items    []domain.RPUsageDailyMetric `json:"items"`
		Days     int                         `json:"days"`
		Period   string                      `json:"period"`
		TenantID string                      `json:"tenantId"`
	}
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	require.Equal(t, 7, body.Days)
	require.Equal(t, "week", body.Period)
	require.Equal(t, "tenant-1", body.TenantID)
	require.Len(t, body.Items, 1)
	require.Equal(t, "orgfront", body.Items[0].ClientID)
	require.Equal(t, uint64(12), body.Items[0].LoginRequests)
}

func TestAdminHandler_UserProjectionStatusRequiresSuperAdmin(t *testing.T) {
	h := &AdminHandler{
		UserProjectionRepo: &fakeAdminUserProjectionRepo{
			status: domain.UserProjectionStatus{Name: domain.UserProjectionNameKratos, Status: domain.UserProjectionStatusReady, Ready: true},
		},
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "tenant-admin", Role: domain.RoleTenantAdmin})
		return c.Next()
	})
	app.Get("/api/v1/admin/projections/users", h.GetUserProjectionStatus)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/projections/users", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusForbidden, resp.StatusCode)
}

func TestAdminHandler_UserProjectionStatusReturnsProjectionStateForSuperAdmin(t *testing.T) {
	syncedAt := time.Date(2026, 5, 11, 3, 0, 0, 0, time.UTC)
	h := &AdminHandler{
		UserProjectionRepo: &fakeAdminUserProjectionRepo{
			status: domain.UserProjectionStatus{
				Name:           domain.UserProjectionNameKratos,
				Status:         domain.UserProjectionStatusReady,
				Ready:          true,
				LastSyncedAt:   &syncedAt,
				ProjectedUsers: 152,
			},
		},
	}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "super", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Get("/api/v1/admin/projections/users", h.GetUserProjectionStatus)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/projections/users", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)

	var body domain.UserProjectionStatus
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	require.Equal(t, domain.UserProjectionNameKratos, body.Name)
	require.Equal(t, domain.UserProjectionStatusReady, body.Status)
	require.True(t, body.Ready)
	require.Equal(t, int64(152), body.ProjectedUsers)
}

func TestAdminHandler_ReconcileUserProjectionRequiresSuperAdminAndRunsSyncer(t *testing.T) {
	syncer := &fakeAdminUserProjectionSyncer{count: 4}
	h := &AdminHandler{UserProjectionSyncer: syncer}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "super", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/admin/projections/users/reconcile", h.ReconcileUserProjection)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/projections/users/reconcile", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Equal(t, 1, syncer.calls)

	var body map[string]any
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	require.Equal(t, "success", body["status"])
	require.Equal(t, float64(4), body["syncedUsers"])
}

func TestAdminHandler_ReconcileUserProjectionReturnsServiceUnavailableOnSyncFailure(t *testing.T) {
	syncer := &fakeAdminUserProjectionSyncer{err: errors.New("kratos down")}
	h := &AdminHandler{UserProjectionSyncer: syncer}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "super", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/admin/projections/users/reconcile", h.ReconcileUserProjection)

	req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/projections/users/reconcile", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
}

func TestAdminHandler_GetRPUsageDailyChecksTenantPermission(t *testing.T) {
	repo := &fakeRPUsageQueryRepo{}
	keto := &fakeAdminKeto{allowed: true}
	h := &AdminHandler{RPUsageQueries: repo, Keto: keto}
	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:   "user-1",
			Role: domain.RoleTenantAdmin,
		})
		return c.Next()
	})
	app.Get("/api/v1/admin/rp-usage/daily", h.GetRPUsageDaily)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/rp-usage/daily?tenantId=tenant-allowed", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)
	require.Equal(t, "User:user-1", keto.subject)
	require.Equal(t, "tenant-allowed", keto.object)
	require.Equal(t, "view_rp_usage_stats", keto.relation)
	require.Equal(t, "tenant-allowed", repo.query.TenantID)
}

func TestAdminHandler_GetSystemStatsIncludesOverviewMetrics(t *testing.T) {
	auditRepo := &fakeOverviewAuditRepo{count: 22}
	h := &AdminHandler{
		AuditRepo: auditRepo,
		UserProjectionRepo: &fakeAdminUserProjectionRepo{
			status: domain.UserProjectionStatus{
				Name:           domain.UserProjectionNameKratos,
				Status:         domain.UserProjectionStatusReady,
				Ready:          true,
				ProjectedUsers: 152,
			},
		},
	}
	app := fiber.New()
	app.Get("/api/v1/admin/stats", h.GetSystemStats)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/stats", nil)
	resp, err := app.Test(req)
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, resp.StatusCode)

	var body map[string]any
	require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	require.Contains(t, body, "totalTenants")
	require.Contains(t, body, "totalUsers")
	require.Contains(t, body, "oidcClients")
	require.Contains(t, body, "auditEvents24h")
	require.Equal(t, float64(152), body["totalUsers"])
	require.Equal(t, float64(22), body["auditEvents24h"])
	require.Equal(t, time.UTC, auditRepo.since.Location())
}
