package handler

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/service"
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/gofiber/fiber/v2"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
)

func TestDevHandler_Isolation(t *testing.T) {
	mockKeto := new(devMockKetoService)
	// Default Mock behavior: deny everything unless explicitly allowed
	mockKeto.On("CheckPermission", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(false, nil).Maybe()

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL: "http://hydra.test",
			HTTPClient: &http.Client{
				Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
					if r.Method == http.MethodGet && r.URL.Path == "/clients" {
						return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
							{
								"client_id":                  "client-tenant-a",
								"client_name":                "App Tenant A",
								"token_endpoint_auth_method": "none", // PKCE
								"metadata":                   map[string]interface{}{"tenant_id": "tenant-a"},
							},
							{
								"client_id":                  "client-tenant-b",
								"client_name":                "App Tenant B",
								"token_endpoint_auth_method": "none", // PKCE
								"metadata":                   map[string]interface{}{"tenant_id": "tenant-b"},
							},
						}), nil
					}
					if (r.Method == http.MethodGet || r.Method == http.MethodPut) && strings.HasPrefix(r.URL.Path, "/clients/") {
						id := strings.TrimPrefix(r.URL.Path, "/clients/")
						tenantID := "tenant-a"
						if id == "client-tenant-b" {
							tenantID = "tenant-b"
						}
						return httpJSONAny(r, http.StatusOK, map[string]interface{}{
							"client_id":                  id,
							"client_name":                "App " + id,
							"token_endpoint_auth_method": "none",
							"metadata":                   map[string]interface{}{"tenant_id": tenantID},
						}), nil
					}
					if r.Method == http.MethodPost && r.URL.Path == "/clients" {
						var body map[string]interface{}
						json.NewDecoder(r.Body).Decode(&body)
						return httpJSONAny(r, http.StatusCreated, body), nil
					}
					return httpJSONAny(r, http.StatusNotFound, nil), nil
				}),
			},
		},
		Keto: mockKeto,
	}

	t.Run("Local bypass should be removed", func(t *testing.T) {
		app := fiber.New()
		app.Get("/api/v1/dev/clients", h.ListClients)

		req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
		req.Header.Set("Origin", "http://localhost:5174")

		resp, _ := app.Test(req, -1)
		assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
	})

	t.Run("ListClients should filter by tenant_id for non-SuperAdmin", func(t *testing.T) {
		app := fiber.New()
		tenantA := "tenant-a"
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{
				ID:       "user-a",
				Role:     domain.RoleTenantAdmin,
				TenantID: &tenantA,
			})
			return c.Next()
		})
		app.Get("/api/v1/dev/clients", h.ListClients)

		// Explicit permission for private client check bypass
		mockKeto.On("CheckPermission", mock.Anything, "User:user-a", "System", "global", "manage_all").Return(true, nil).Once()
		mockKeto.On(
			"ListRelations",
			mock.Anything,
			"RelyingParty",
			mock.Anything,
			mock.Anything,
			mock.Anything,
		).Return([]service.RelationTuple{}, nil).Maybe()

		req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
		resp, _ := app.Test(req, -1)

		assert.Equal(t, http.StatusOK, resp.StatusCode)
		var res struct {
			Items []clientSummary `json:"items"`
		}
		json.NewDecoder(resp.Body).Decode(&res)

		// Should only see client-tenant-a (tenant isolation)
		assert.Equal(t, 1, len(res.Items))
		assert.Equal(t, "client-tenant-a", res.Items[0].ID)
	})

	t.Run("Tenant member should see empty list from DevFront clients if no relation", func(t *testing.T) {
		app := fiber.New()
		tenantA := "tenant-a"
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{
				ID:       "user-member",
				Role:     domain.RoleUser,
				TenantID: &tenantA,
			})
			return c.Next()
		})
		app.Get("/api/v1/dev/clients", h.ListClients)

		req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
		resp, _ := app.Test(req, -1)
		assert.Equal(t, http.StatusOK, resp.StatusCode)

		var res struct {
			Items []clientSummary `json:"items"`
		}
		json.NewDecoder(resp.Body).Decode(&res)
		// Empty list because we didn't mock any specific 'view' permissions for this user
		assert.Equal(t, 0, len(res.Items))
	})

	t.Run("RP Admin should only see managed clients", func(t *testing.T) {
		app := fiber.New()
		tenantA := "tenant-a"
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{
				ID:       "rp-admin-a",
				Role:     domain.RoleRPAdmin,
				TenantID: &tenantA,
				Metadata: map[string]any{
					"managed_client_ids": []any{"client-tenant-a"},
				},
			})
			return c.Next()
		})
		app.Get("/api/v1/dev/clients", h.ListClients)

		req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients", nil)
		resp, _ := app.Test(req, -1)

		assert.Equal(t, http.StatusOK, resp.StatusCode)
		var res struct {
			Items []clientSummary `json:"items"`
		}
		json.NewDecoder(resp.Body).Decode(&res)
		assert.Equal(t, 1, len(res.Items))
		assert.Equal(t, "client-tenant-a", res.Items[0].ID)
	})

	t.Run("GetClient should enforce tenant isolation", func(t *testing.T) {
		app := fiber.New()
		tenantA := "tenant-a"
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{
				ID:       "user-a",
				Role:     domain.RoleTenantAdmin,
				TenantID: &tenantA,
			})
			return c.Next()
		})
		app.Get("/api/v1/dev/clients/:id", h.GetClient)

		// Case 1: Same tenant
		req := httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-tenant-a", nil)
		resp, _ := app.Test(req, -1)
		assert.Equal(t, http.StatusOK, resp.StatusCode)

		// Case 2: Different tenant
		req = httptest.NewRequest(http.MethodGet, "/api/v1/dev/clients/client-tenant-b", nil)
		resp, _ = app.Test(req, -1)
		assert.Equal(t, http.StatusForbidden, resp.StatusCode)
	})

	t.Run("UpdateClient should require direct edit permission within tenant isolation", func(t *testing.T) {
		app := fiber.New()
		tenantA := "tenant-a"
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{
				ID:       "user-a",
				Role:     domain.RoleTenantAdmin,
				TenantID: &tenantA,
			})
			return c.Next()
		})
		app.Put("/api/v1/dev/clients/:id", h.UpdateClient)

		body, _ := json.Marshal(map[string]interface{}{
			"client_name": "Updated Name",
		})

		// Case 1: Same tenant but no direct edit_config permission
		req := httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-tenant-a", bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		resp, _ := app.Test(req, -1)
		assert.Equal(t, http.StatusForbidden, resp.StatusCode)

		// Case 2: Different tenant
		req = httptest.NewRequest(http.MethodPut, "/api/v1/dev/clients/client-tenant-b", bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		resp, _ = app.Test(req, -1)
		assert.Equal(t, http.StatusForbidden, resp.StatusCode)
	})

	t.Run("CreateClient should record user_id and tenant_id", func(t *testing.T) {
		app := fiber.New()
		tenantA := "tenant-a"
		app.Use(func(c *fiber.Ctx) error {
			c.Locals("user_profile", &domain.UserProfileResponse{
				ID:       "user-a",
				Role:     domain.RoleSuperAdmin, // Bypass for creation permission
				TenantID: &tenantA,
			})
			return c.Next()
		})
		app.Post("/api/v1/dev/clients", h.CreateClient)

		body, _ := json.Marshal(map[string]interface{}{
			"client_name":  "New App",
			"type":         "pkce",
			"redirectUris": []string{"http://localhost/cb"},
		})
		req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("X-Tenant-ID", "tenant-a")

		resp, _ := app.Test(req, -1)
		assert.Equal(t, http.StatusCreated, resp.StatusCode)

		var res clientDetailResponse
		json.NewDecoder(resp.Body).Decode(&res)

		assert.Equal(t, "tenant-a", res.Client.Metadata["tenant_id"])
		assert.Equal(t, "user-a", res.Client.Metadata["user_id"])
	})
}
