package oci

import (
	"context"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/oracle/oci-go-sdk/v65/common"
	"github.com/oracle/oci-go-sdk/v65/identitydomains"
)

// TestRealOCIFederation 验证 Federation 全链路：下载域元数据 → 创建 IdP
// （默认 JIT）→ 激活并上登录页 → 免 MFA 规则置顶 → 逐级回滚清理。
// 只操作本测试创建的资源，现有 IdP 与规则只做顺延断言。
func TestRealOCIFederation(t *testing.T) {
	if os.Getenv("OCI_INTEGRATION_TEST") != "1" {
		t.Skip("set OCI_INTEGRATION_TEST=1 to run real OCI tests")
	}
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	defer cancel()
	cred := loadTestCredentials(t, "试用期")
	client := NewClient()
	region := cred.Region

	metadata, err := client.DownloadDomainSamlMetadata(ctx, cred, region, "")
	if err != nil {
		t.Fatalf("DownloadDomainSamlMetadata: %v", err)
	}
	if !strings.Contains(string(metadata), "EntityDescriptor") {
		t.Fatalf("metadata is not SAML XML: %s", string(metadata[:min(len(metadata), 120)]))
	}
	t.Logf("domain saml metadata: %d bytes", len(metadata))

	before, err := client.ListConsoleSignOnRules(ctx, cred, region, "")
	if err != nil {
		t.Fatalf("ListConsoleSignOnRules: %v", err)
	}
	logRules(t, "before", before)

	idp := createTestIdp(ctx, t, client, cred, region)
	verifyIdpDefaults(ctx, t, client, cred, region, idp.ID)

	activated, err := client.SetIdentityProviderEnabled(ctx, cred, region, "", idp.ID, true)
	if err != nil {
		t.Fatalf("activate idp: %v", err)
	}
	if !activated.Enabled {
		t.Error("idp not enabled after activate")
	}
	assertLoginPage(ctx, t, client, cred, region, idp.ID, true)

	exemptionRoundTrip(ctx, t, client, cred, region, idp.ID, len(before))

	deactivated, err := client.SetIdentityProviderEnabled(ctx, cred, region, "", idp.ID, false)
	if err != nil {
		t.Fatalf("deactivate idp: %v", err)
	}
	if deactivated.Enabled {
		t.Error("idp still enabled after deactivate")
	}
	assertLoginPage(ctx, t, client, cred, region, idp.ID, false)
}

func createTestIdp(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, region string) IdentityProviderInfo {
	t.Helper()
	metadata, err := os.ReadFile("../../test-idp.xml")
	if err != nil {
		t.Fatalf("read test-idp.xml: %v", err)
	}
	name := fmt.Sprintf("oci-portal-e2e-idp-%d", time.Now().Unix())
	idp, err := client.CreateSamlIdentityProvider(ctx, cred, region, "", CreateIdpInput{
		Name:        name,
		Metadata:    string(metadata),
		Description: "oci-portal e2e temporary idp",
	})
	if err != nil {
		t.Fatalf("CreateSamlIdentityProvider: %v", err)
	}
	t.Logf("idp created: %s (%s) partner=%s", idp.Name, idp.ID, idp.PartnerProviderID)
	if idp.Enabled {
		t.Error("new idp should be disabled")
	}
	t.Cleanup(func() {
		cctx, ccancel := context.WithTimeout(context.Background(), 2*time.Minute)
		defer ccancel()
		if err := client.DeleteIdentityProvider(cctx, cred, region, "", idp.ID); err != nil {
			t.Errorf("cleanup: delete idp: %v", err)
		} else {
			t.Log("cleanup: idp deleted")
		}
	})
	return idp
}

// verifyIdpDefaults 回读 IdP 断言控制台默认值与 JIT 属性映射两条。
func verifyIdpDefaults(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, region, idpID string) {
	t.Helper()
	dc, err := client.domainsClient(ctx, cred, region, "")
	if err != nil {
		t.Fatalf("domainsClient: %v", err)
	}
	got, err := dc.GetIdentityProvider(ctx, identitydomains.GetIdentityProviderRequest{IdentityProviderId: &idpID})
	if err != nil {
		t.Fatalf("get idp: %v", err)
	}
	if deref(got.NameIdFormat) != "saml-none" {
		t.Errorf("nameIdFormat = %q, want saml-none", deref(got.NameIdFormat))
	}
	if got.UserMappingMethod != identitydomains.IdentityProviderUserMappingMethodNameidtouserattribute || deref(got.UserMappingStoreAttribute) != "userName" {
		t.Errorf("user mapping = %s/%s, want NameIDToUserAttribute/userName", got.UserMappingMethod, deref(got.UserMappingStoreAttribute))
	}
	if got.JitUserProvEnabled == nil || !*got.JitUserProvEnabled || got.JitUserProvCreateUserEnabled == nil || !*got.JitUserProvCreateUserEnabled {
		t.Error("jit enable/create should be true")
	}
	if got.JitUserProvAttributeUpdateEnabled != nil && *got.JitUserProvAttributeUpdateEnabled {
		t.Error("jit attribute update should be false")
	}
	if len(got.JitUserProvAssignedGroups) != 1 || deref(got.JitUserProvAssignedGroups[0].Display) != "Administrators" {
		t.Errorf("jit assigned groups = %+v, want [Administrators]", got.JitUserProvAssignedGroups)
	}
	verifyJitMappings(ctx, t, dc, got.JitUserProvAttributes)
}

func verifyJitMappings(ctx context.Context, t *testing.T, dc identitydomains.IdentityDomainsClient, ref *identitydomains.IdentityProviderJitUserProvAttributes) {
	t.Helper()
	if ref == nil || ref.Value == nil {
		t.Error("idp has no jit attribute mapping resource")
		return
	}
	ma, err := dc.GetMappedAttribute(ctx, identitydomains.GetMappedAttributeRequest{MappedAttributeId: ref.Value})
	if err != nil {
		t.Errorf("get mapped attribute: %v", err)
		return
	}
	want := map[string]bool{"userName": false, "name.familyName": false}
	for _, m := range ma.AttributeMappings {
		if deref(m.ManagedObjectAttributeName) == "$(assertion.fed.nameidvalue)" {
			want[deref(m.IdcsAttributeName)] = true
		}
	}
	for attr, seen := range want {
		if !seen {
			t.Errorf("jit mapping NameID value → %s missing; got %+v", attr, ma.AttributeMappings)
		}
	}
}

// assertLoginPage 校验 DefaultIDPRule 的 SamlIDPs 是否包含目标 IdP。
func assertLoginPage(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, region, idpID string, want bool) {
	t.Helper()
	dc, err := client.domainsClient(ctx, cred, region, "")
	if err != nil {
		t.Fatalf("domainsClient: %v", err)
	}
	rule, err := dc.GetRule(ctx, identitydomains.GetRuleRequest{RuleId: common.String(defaultIdpRuleID)})
	if err != nil {
		t.Fatalf("get default idp rule: %v", err)
	}
	for _, r := range rule.Return {
		if deref(r.Name) != "SamlIDPs" {
			continue
		}
		has := strings.Contains(deref(r.Value), idpID)
		if has != want {
			t.Errorf("login page SamlIDPs contains idp = %v, want %v (value=%s)", has, want, deref(r.Value))
		}
		return
	}
	t.Error("DefaultIDPRule has no SamlIDPs return")
}

// exemptionRoundTrip 创建免 MFA 规则断言置顶与顺延，删除后断言完全复位。
func exemptionRoundTrip(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, region, idpID string, beforeCount int) {
	t.Helper()
	rule, err := client.CreateMfaExemptionRule(ctx, cred, region, "", idpID, "oci-portal-e2e-skip-mfa")
	if err != nil {
		t.Fatalf("CreateMfaExemptionRule: %v", err)
	}
	t.Logf("exemption rule created: %s (%s)", rule.Name, rule.ID)
	deleted := false
	defer func() {
		if deleted {
			return
		}
		if err := client.DeleteMfaExemptionRule(ctx, cred, region, "", rule.ID); err != nil {
			t.Errorf("cleanup: delete exemption rule: %v", err)
		}
	}()

	after, err := client.ListConsoleSignOnRules(ctx, cred, region, "")
	if err != nil {
		t.Fatalf("ListConsoleSignOnRules after create: %v", err)
	}
	logRules(t, "after-create", after)
	if len(after) != beforeCount+1 {
		t.Errorf("rule count = %d, want %d", len(after), beforeCount+1)
	}
	if after[0].ID != rule.ID || after[0].Sequence != 1 {
		t.Errorf("top rule = %s seq=%d, want %s seq=1", after[0].ID, after[0].Sequence, rule.ID)
	}
	if after[0].AuthenticationFactor != "IDP" {
		t.Errorf("authenticationFactor = %s, want IDP", after[0].AuthenticationFactor)
	}
	if !strings.Contains(after[0].ConditionValue, idpID) {
		t.Errorf("condition value %q does not reference idp", after[0].ConditionValue)
	}

	if err := client.DeleteMfaExemptionRule(ctx, cred, region, "", rule.ID); err != nil {
		t.Fatalf("DeleteMfaExemptionRule: %v", err)
	}
	deleted = true
	restored, err := client.ListConsoleSignOnRules(ctx, cred, region, "")
	if err != nil {
		t.Fatalf("ListConsoleSignOnRules after delete: %v", err)
	}
	logRules(t, "after-delete", restored)
	if len(restored) != beforeCount {
		t.Errorf("rule count after delete = %d, want %d", len(restored), beforeCount)
	}
	for _, r := range restored {
		if r.ID == rule.ID {
			t.Error("exemption rule still present after delete")
		}
	}
}

func logRules(t *testing.T, tag string, rules []SignOnRuleInfo) {
	t.Helper()
	for _, r := range rules {
		t.Logf("%s: seq=%d %s (%s) factor=%s", tag, r.Sequence, r.Name, r.ID, r.AuthenticationFactor)
	}
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
