package handler

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

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

func TestCreateClient_NormalizesTenantAccessMetadata(t *testing.T) {
	var captured domain.HydraClient
	ownerTenantID := "tenant-owner"

	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			body, err := io.ReadAll(r.Body)
			assert.NoError(t, err)
			assert.NoError(t, json.Unmarshal(body, &captured))
			return httpJSONAny(r, http.StatusCreated, map[string]any{
				"client_id":                  captured.ClientID,
				"client_name":                captured.ClientName,
				"redirect_uris":              captured.RedirectURIs,
				"grant_types":                captured.GrantTypes,
				"response_types":             captured.ResponseTypes,
				"scope":                      captured.Scope,
				"token_endpoint_auth_method": captured.TokenEndpointAuthMethod,
				"metadata":                   captured.Metadata,
			}), nil
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{
			ID:       "user-1",
			Role:     domain.RoleSuperAdmin,
			TenantID: &ownerTenantID,
		})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"id":           "client-tenant",
		"name":         "Tenant Client",
		"type":         "pkce",
		"redirectUris": []string{"https://rp.example.com/cb"},
		"metadata": map[string]any{
			"tenant_access_restricted": true,
			"allowed_tenants":          []string{"tenant-b", "tenant-a", "tenant-b"},
		},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	assert.True(t, clientTenantAccessRestricted(captured.Metadata))
	assert.Equal(t, []string{"tenant-a", "tenant-b", "tenant-owner"}, clientAllowedTenants(captured.Metadata))
}

func TestCreateClient_RejectsTenantAccessWithoutAllowedTenants(t *testing.T) {
	hydraCalled := false
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		if r.Method == http.MethodPost && r.URL.Path == "/clients" {
			hydraCalled = true
		}
		return httpJSONAny(r, http.StatusNotFound, nil), nil
	})

	h := &DevHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: &http.Client{Transport: transport},
		},
		Keto: new(devMockKetoService),
	}

	app := fiber.New()
	app.Use(func(c *fiber.Ctx) error {
		c.Locals("user_profile", &domain.UserProfileResponse{ID: "user-1", Role: domain.RoleSuperAdmin})
		return c.Next()
	})
	app.Post("/api/v1/dev/clients", h.CreateClient)

	body, _ := json.Marshal(map[string]any{
		"id":           "client-tenant",
		"name":         "Tenant Client",
		"type":         "pkce",
		"redirectUris": []string{"https://rp.example.com/cb"},
		"metadata": map[string]any{
			"tenant_access_restricted": true,
		},
	})
	req := httptest.NewRequest(http.MethodPost, "/api/v1/dev/clients", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")

	resp, err := app.Test(req, -1)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
	assert.False(t, hydraCalled)
}

func TestMergeRequestedScopesWithClientRequirements_AddsTenantScope(t *testing.T) {
	client := domain.HydraClient{
		Metadata: map[string]any{
			"tenant_access_restricted": true,
			"structured_scopes": []map[string]any{
				{"name": "openid", "mandatory": true},
				{"name": "tenant", "mandatory": true, "locked": true},
				{"name": "profile", "mandatory": false},
			},
		},
	}

	merged := mergeRequestedScopesWithClientRequirements(client, []string{"openid", "profile"})
	assert.Equal(t, []string{"openid", "tenant", "profile"}, merged)
}

func TestMergeRequestedScopesWithClientRequirements_StripsRefreshTokenScopeAliases(t *testing.T) {
	client := domain.HydraClient{
		Metadata: map[string]any{
			"tenant_access_restricted": true,
			"structured_scopes": []map[string]any{
				{"name": "offline", "mandatory": true},
				{"name": "offline_access", "locked": true},
				{"name": "email", "mandatory": true},
			},
		},
	}

	merged := mergeRequestedScopesWithClientRequirements(
		client,
		[]string{"openid", "offline", "profile", "offline_access"},
	)

	assert.Equal(t, []string{"openid", "tenant", "profile", "offline_access", "email"}, merged)
}

func TestBuildHydraAuthorizationURL_StripsRefreshTokenScopeAliases(t *testing.T) {
	urlString := buildHydraAuthorizationURL(
		"client-refresh",
		[]string{"offline", "profile", "offline_access", "email"},
		[]string{"https://rp.example.com/callback"},
	)

	parsed, err := url.Parse(urlString)
	assert.NoError(t, err)
	scopes := parsed.Query().Get("scope")
	scopeItems := strings.Fields(scopes)

	assert.Equal(t, "openid profile offline_access email", scopes)
	assert.NotContains(t, scopeItems, "offline")
	assert.Contains(t, scopeItems, "offline_access")
}

func TestGetConsentRequest_DeniesTenantAccess(t *testing.T) {
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		switch {
		case r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-tenant":
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       "challenge-tenant",
				"requested_scope": []string{"openid", "profile"},
				"skip":            false,
				"subject":         "user-123",
				"client": map[string]any{
					"client_id": "client-tenant",
					"metadata": map[string]any{
						"tenant_access_restricted": true,
						"allowed_tenants":          []string{"tenant-b"},
					},
				},
			}), nil
		case r.URL.Host == "kratos.test" && r.URL.Path == "/sessions/whoami":
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"identity": map[string]any{
					"id": "user-123",
					"traits": map[string]any{
						"email":     "user@test.com",
						"tenant_id": "tenant-a",
					},
				},
			}), nil
		default:
			return httpJSONAny(r, http.StatusNotFound, nil), nil
		}
	})

	client := &http.Client{Transport: transport}
	origDefault := http.DefaultClient
	http.DefaultClient = client
	defer func() { http.DefaultClient = origDefault }()

	t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")

	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
	}

	app := fiber.New()
	app.Get("/api/v1/auth/consent", h.GetConsentRequest)

	t.Setenv("APP_ENV", "dev")
	req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/consent?consent_challenge=challenge-tenant", nil)
	req.Header.Set("X-Mock-Role", "user")
	req.Header.Set("X-Tenant-ID", "tenant-a")

	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusForbidden, resp.StatusCode)

	var body map[string]any
	assert.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	assert.Equal(t, "tenant_not_allowed", body["code"])
	details, ok := body["details"].(map[string]any)
	assert.True(t, ok)
	account, ok := details["account"].(map[string]any)
	assert.True(t, ok)
	assert.NotEmpty(t, account["email"])
	currentTenant, ok := details["current_tenant"].(map[string]any)
	assert.True(t, ok)
	assert.NotEmpty(t, currentTenant["identifier"])
}

func TestGetConsentRequest_DeniesRestrictedClientWhenProfileResolutionFails(t *testing.T) {
	acceptCalled := false
	transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
		switch {
		case r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-profile-missing":
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"challenge":       "challenge-profile-missing",
				"requested_scope": []string{"openid", "profile"},
				"skip":            false,
				"subject":         "user-123",
				"client": map[string]any{
					"client_id": "client-tenant",
					"metadata": map[string]any{
						"tenant_access_restricted": true,
						"allowed_tenants":          []string{"tenant-b"},
					},
				},
			}), nil
		case r.URL.Path == "/oauth2/auth/requests/consent/accept":
			acceptCalled = true
			return httpJSONAny(r, http.StatusOK, map[string]any{
				"redirect_to": "http://rp/cb",
			}), nil
		default:
			return httpJSONAny(r, http.StatusNotFound, nil), nil
		}
	})

	client := &http.Client{Transport: transport}
	origDefault := http.DefaultClient
	http.DefaultClient = client
	defer func() { http.DefaultClient = origDefault }()

	t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")

	h := &AuthHandler{
		Hydra: &service.HydraAdminService{
			AdminURL:   "http://hydra.test",
			HTTPClient: client,
		},
		KratosAdmin: func() service.KratosAdminService {
			mockKratos := new(MockKratosAdminService)
			mockKratos.On("GetIdentity", mock.Anything, "user-123").Return(&service.KratosIdentity{
				ID: "user-123",
				Traits: map[string]any{
					"email":       "user@test.com",
					"tenant_id":   "tenant-a",
					"companyCode": "tenant-a",
				},
			}, nil).Once()
			return mockKratos
		}(),
		TenantService: func() service.TenantService {
			tenantSvc := new(MockTenantService)
			tenantSvc.On("GetTenant", mock.Anything, "tenant-a").Return(&domain.Tenant{
				ID:   "tenant-a",
				Slug: "tenant-a",
				Name: "Tenant A",
			}, nil)
			tenantSvc.On("GetTenant", mock.Anything, "tenant-c").Return(&domain.Tenant{
				ID:   "tenant-c",
				Slug: "tenant-c",
				Name: "Tenant C",
			}, nil)
			tenantSvc.On("ListJoinedTenants", mock.Anything, "user-123").Return([]domain.Tenant{
				{ID: "tenant-a", Slug: "tenant-a", Name: "Tenant A"},
				{ID: "tenant-c", Slug: "tenant-c", Name: "Tenant C"},
			}, nil).Once()
			tenantSvc.On("GetTenant", mock.Anything, "tenant-b").Return(nil, assert.AnError)
			tenantSvc.On("GetTenantBySlug", mock.Anything, "tenant-b").Return(&domain.Tenant{
				ID:   "tenant-b-id",
				Slug: "tenant-b",
				Name: "Tenant B",
			}, nil)
			return tenantSvc
		}(),
		ConsentRepo: &mockConsentRepo{
			consents: []domain.ClientConsent{
				{
					ClientID:      "client-tenant",
					Subject:       "user-123",
					GrantedScopes: []string{"openid", "profile"},
				},
			},
		},
	}

	app := fiber.New()
	app.Get("/api/v1/auth/consent", h.GetConsentRequest)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/consent?consent_challenge=challenge-profile-missing", nil)
	req.Header.Set("Cookie", "ory_kratos_session=invalid-session")

	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusForbidden, resp.StatusCode)
	assert.False(t, acceptCalled)

	var body map[string]any
	assert.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	assert.Equal(t, "tenant_not_allowed", body["code"])

	details, ok := body["details"].(map[string]any)
	assert.True(t, ok)
	account, ok := details["account"].(map[string]any)
	assert.True(t, ok)
	assert.Equal(t, "user@test.com", account["email"])
	currentTenant, ok := details["current_tenant"].(map[string]any)
	assert.True(t, ok)
	assert.Equal(t, "Tenant A", currentTenant["name"])
	affiliatedTenants, ok := details["affiliated_tenants"].([]any)
	assert.True(t, ok)
	assert.Len(t, affiliatedTenants, 2)
}

func TestAcceptOidcLoginRequest_DeniesTenantAccess(t *testing.T) {
	app := fiber.New()
	app.Get("/deny", func(c *fiber.Ctx) error {
		tenantID := "tenant-a"
		profile := &domain.UserProfileResponse{
			ID:          "user-123",
			Role:        domain.RoleUser,
			Email:       "user@test.com",
			TenantID:    &tenantID,
			CompanyCode: "tenant-a",
			JoinedTenants: []domain.Tenant{
				{ID: "tenant-a", Slug: "tenant-a", Name: "Tenant A"},
				{ID: "tenant-c", Slug: "tenant-c", Name: "Tenant C"},
			},
		}
		client := domain.HydraClient{
			ClientID: "client-tenant",
			Metadata: map[string]any{
				"tenant_access_restricted": true,
				"allowed_tenants":          []string{"tenant-b"},
			},
		}
		tenantSvc := new(MockTenantService)
		tenantSvc.On("GetTenant", mock.Anything, "tenant-a").Return(&domain.Tenant{
			ID:   "tenant-a",
			Slug: "tenant-a",
			Name: "Tenant A",
		}, nil)
		tenantSvc.On("GetTenant", mock.Anything, "tenant-c").Return(&domain.Tenant{
			ID:   "tenant-c",
			Slug: "tenant-c",
			Name: "Tenant C",
		}, nil)
		tenantSvc.On("GetTenant", mock.Anything, "tenant-b").Return(nil, assert.AnError)
		tenantSvc.On("GetTenantBySlug", mock.Anything, "tenant-b").Return(&domain.Tenant{
			ID:   "tenant-b-id",
			Slug: "tenant-b",
			Name: "Tenant B",
		}, nil)
		enforceClientTenantAccess(c, tenantSvc, client, profile, nil)
		return nil
	})

	req := httptest.NewRequest(http.MethodGet, "/deny", nil)
	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusForbidden, resp.StatusCode)

	var body map[string]any
	assert.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
	assert.Equal(t, "tenant_not_allowed", body["code"])

	details, ok := body["details"].(map[string]any)
	assert.True(t, ok)

	account, ok := details["account"].(map[string]any)
	assert.True(t, ok)
	assert.Equal(t, "user@test.com", account["email"])

	currentTenant, ok := details["current_tenant"].(map[string]any)
	assert.True(t, ok)
	assert.Equal(t, "Tenant A", currentTenant["name"])
	affiliatedTenants, ok := details["affiliated_tenants"].([]any)
	assert.True(t, ok)
	assert.Len(t, affiliatedTenants, 2)

	allowedTenants, ok := details["allowed_tenants"].([]any)
	assert.True(t, ok)
	assert.Len(t, allowedTenants, 1)
	allowedTenant, ok := allowedTenants[0].(map[string]any)
	assert.True(t, ok)
	assert.Equal(t, "Tenant B", allowedTenant["name"])
}

func TestAcceptOidcLoginRequest_AllowsRestrictedClientForHanmacFamilyDescendant(t *testing.T) {
	app := fiber.New()
	app.Get("/allow-descendant", func(c *fiber.Ctx) error {
		hanmacFamilyID := "hanmac-family-id"
		samanID := "saman-id"
		profile := &domain.UserProfileResponse{
			ID:       "user-123",
			Role:     domain.RoleUser,
			Email:    "user@samaneng.com",
			TenantID: &samanID,
			Tenant: &domain.Tenant{
				ID:       samanID,
				Slug:     "saman",
				Name:     "삼안",
				ParentID: &hanmacFamilyID,
			},
			JoinedTenants: []domain.Tenant{
				{
					ID:       samanID,
					Slug:     "saman",
					Name:     "삼안",
					ParentID: &hanmacFamilyID,
				},
			},
		}
		client := domain.HydraClient{
			ClientID: "orgfront",
			Metadata: map[string]any{
				"tenant_access_restricted": true,
				"allowed_tenants":          []string{"hanmac-family"},
			},
		}
		tenantSvc := new(MockTenantService)
		tenantSvc.On("GetTenant", mock.Anything, "hanmac-family").Return(nil, assert.AnError).Maybe()
		tenantSvc.On("GetTenantBySlug", mock.Anything, "hanmac-family").Return(&domain.Tenant{
			ID:   hanmacFamilyID,
			Slug: "hanmac-family",
			Name: "한맥가족",
		}, nil).Maybe()
		tenantSvc.On("GetTenant", mock.Anything, samanID).Return(&domain.Tenant{
			ID:       samanID,
			Slug:     "saman",
			Name:     "삼안",
			ParentID: &hanmacFamilyID,
		}, nil).Maybe()
		tenantSvc.On("GetTenant", mock.Anything, hanmacFamilyID).Return(&domain.Tenant{
			ID:   hanmacFamilyID,
			Slug: "hanmac-family",
			Name: "한맥가족",
		}, nil).Maybe()

		blocked := enforceClientTenantAccess(c, tenantSvc, client, profile, nil)
		assert.False(t, blocked)
		return c.SendStatus(http.StatusNoContent)
	})

	req := httptest.NewRequest(http.MethodGet, "/allow-descendant", nil)
	resp, err := app.Test(req)
	assert.NoError(t, err)
	assert.Equal(t, http.StatusNoContent, resp.StatusCode)
}
