package oci

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

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

func createVolumeRequest(tenancyOCID, ad, name string, size int64) core.CreateVolumeRequest {
	return core.CreateVolumeRequest{CreateVolumeDetails: core.CreateVolumeDetails{
		CompartmentId:      &tenancyOCID,
		AvailabilityDomain: &ad,
		DisplayName:        &name,
		SizeInGBs:          &size,
	}}
}

func getVolumeRequest(volID string) core.GetVolumeRequest {
	return core.GetVolumeRequest{VolumeId: &volID}
}

func deleteVolumeRequest(volID string) core.DeleteVolumeRequest {
	return core.DeleteVolumeRequest{VolumeId: &volID}
}

// e2eConsoleSSHKey 是控制台连接 e2e 用的一次性 RSA 公钥（私钥已丢弃，
// 仅验证连接串生成）；OCI 控制台连接只接受 RSA 公钥。
const e2eConsoleSSHKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXrOBK+eL3JPB3t470k7x+au3IcGbCy3Za7HVcMqTD2XNjar+Te8IND7zjDlrU1DuGL0qa8MFGf8PX84SqK9fQ3kbIuz5KHS+Jk8RYyFSZ1iUs27n3Vkql0tbMj8raCqCiX9nWeu5GSxVu/7UTZG5Tk4mrJXcVe6s+65baQrd6D8V/WKSDBblI8Y0jQ7g2XGsHj5eBV49nCwEtwCWPRzkdNqI8Ac0heH6OcqreuH3GowNSsV9RIe/bHKWpu3Ap6DQHMexTHc5GMmcLu4F43Pij0OaoAA83QVCXLvTMZqyLNQOXxPUEsRWATRKKMKRsvitcWx7+v8WGh06iOHfeE59z oci-portal-e2e"

// TestRealOCIInstanceAutoNetworkAndPublicIP 验证默认可用域 + 自动建网（含
// IPv6 与安全列表全开放）+ 换公网 IP + 添加/取消分配 IPv6 + 控制台连接：
// 不传 availabilityDomain 与 subnetId 创建实例（默认 ad-1、自动建 VCN、分配
// IPv6）→ 等 RUNNING → 换公网 IP → 追加 IPv6 → 控制台连接创建/删除 →
// 删除全部 IPv6 → 清理。A1 容量不足时跳过。
func TestRealOCIInstanceAutoNetworkAndPublicIP(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(), 15*time.Minute)
	defer cancel()
	cred := loadTestCredentials(t, "试用期")
	client := NewClient()
	prefix := fmt.Sprintf("oci-portal-e2e-%d", time.Now().Unix())

	images, err := client.ListImages(ctx, cred, ImagesQuery{OperatingSystem: "Canonical Ubuntu", Shape: "VM.Standard.A1.Flex"})
	if err != nil || len(images) == 0 {
		t.Fatalf("ListImages: %v", err)
	}

	// 不传 availabilityDomain 与 subnetId：应默认 ad-1 并自动建 oci-portal-auto-vcn 与子网
	instance, err := client.LaunchInstance(ctx, cred, CreateInstanceInput{
		DisplayName:       prefix + "-vm",
		Shape:             "VM.Standard.A1.Flex",
		Ocpus:             1,
		MemoryInGBs:       6,
		ImageID:           images[0].ID,
		BootVolumeSizeGBs: 50,
		AssignPublicIP:    true,
		AssignIpv6:        true,
		RootPassword:      "OciPortalE2E!" + fmt.Sprint(len(prefix)),
	})
	if err != nil {
		if strings.Contains(err.Error(), "capacity") {
			t.Skipf("A1 capacity unavailable: %v", err)
		}
		t.Fatalf("LaunchInstance (auto network): %v", err)
	}
	if !strings.HasSuffix(instance.AvailabilityDomain, "AD-1") {
		t.Errorf("availability domain = %q, want default AD-1", instance.AvailabilityDomain)
	}
	t.Logf("launched %s in %s with auto network", instance.ID, instance.AvailabilityDomain)

	// 确认自动建网产生了 oci-portal-auto-vcn，且 IPv6 与安全列表全开放生效
	vcns, err := client.ListVCNs(ctx, cred, "")
	if err != nil {
		t.Fatalf("ListVCNs: %v", err)
	}
	autoVcnID := ""
	for _, v := range vcns {
		if v.DisplayName == autoVcnName {
			autoVcnID = v.ID
			checkAutoVCNDefaults(ctx, t, client, cred, v)
		}
	}
	if autoVcnID == "" {
		t.Error("auto vcn not created")
	} else {
		// 先注册 VCN 清理（LIFO 后执行），确保实例先终止再删网络
		defer cleanupVCN(t, client, cred, autoVcnID)
	}
	defer terminateAndWait(t, client, cred, instance.ID)

	if err := waitInstanceState(ctx, client, cred, instance.ID, "RUNNING"); err != nil {
		t.Fatalf("wait RUNNING: %v", err)
	}

	got, err := client.GetInstance(ctx, cred, "", instance.ID)
	if err != nil {
		t.Fatalf("GetInstance: %v", err)
	}
	oldIP := got.PublicIP
	if oldIP == "" {
		t.Fatal("no public ip on primary vnic, want one")
	}
	if len(got.Ipv6Addresses) == 0 {
		t.Error("no ipv6 address on instance, want auto network ipv6")
	}
	t.Logf("public=%s ipv6=%v", oldIP, got.Ipv6Addresses)

	newIP, err := client.ChangeInstancePublicIP(ctx, cred, "", instance.ID)
	if err != nil {
		t.Fatalf("ChangeInstancePublicIP: %v", err)
	}
	t.Logf("public ip changed: %s -> %s", oldIP, newIP)
	if newIP == "" || newIP == oldIP {
		t.Errorf("new public ip = %q, want a different non-empty address", newIP)
	}

	// 追加一个自动分配的 IPv6，应与 launch 分配的共存
	added, err := client.AddInstanceIpv6(ctx, cred, "", instance.ID, "")
	if err != nil {
		t.Fatalf("AddInstanceIpv6: %v", err)
	}
	t.Logf("ipv6 added: %s", added)
	withAdded, err := client.GetInstance(ctx, cred, "", instance.ID)
	if err != nil {
		t.Fatalf("GetInstance after ipv6 add: %v", err)
	}
	if len(withAdded.Ipv6Addresses) != 2 {
		t.Errorf("ipv6 addresses = %v, want 2 after add", withAdded.Ipv6Addresses)
	}

	verifyConsoleConnection(ctx, t, client, cred, instance.ID)
	deleteInstanceIpv6AndVerify(ctx, t, client, cred, instance.ID, withAdded.Ipv6Addresses)
}

// verifyConsoleConnection 创建控制台连接、校验 VNC 连接串并删除。
func verifyConsoleConnection(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, instanceID string) {
	t.Helper()
	conn, err := client.CreateConsoleConnection(ctx, cred, "", instanceID, e2eConsoleSSHKey)
	if err != nil {
		t.Fatalf("CreateConsoleConnection: %v", err)
	}
	if conn.LifecycleState != "ACTIVE" || conn.VncConnectionString == "" {
		t.Errorf("console connection state=%s vnc=%q, want ACTIVE with vnc string", conn.LifecycleState, conn.VncConnectionString)
	}
	t.Logf("console connection %s state=%s", conn.ID, conn.LifecycleState)
	list, err := client.ListConsoleConnections(ctx, cred, "", instanceID)
	if err != nil || len(list) == 0 {
		t.Errorf("ListConsoleConnections: %v (got %d)", err, len(list))
	}
	if err := client.DeleteConsoleConnection(ctx, cred, "", conn.ID); err != nil {
		t.Errorf("DeleteConsoleConnection: %v", err)
	} else {
		t.Log("console connection deleted")
	}
}

// checkAutoVCNDefaults 校验自动 VCN 已分配 IPv6 /56 且默认安全列表全开放。
// OCI 在异步分配 IPv6 时可能追加缺省 ICMPv6 规则，因此按语义而非数量校验。
func checkAutoVCNDefaults(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, vcn VCN) {
	t.Helper()
	if len(vcn.Ipv6CidrBlocks) == 0 {
		t.Error("auto vcn has no ipv6 cidr, want one")
	}
	sl, err := client.GetSecurityList(ctx, cred, "", vcn.DefaultSecurityListID)
	if err != nil {
		t.Errorf("GetSecurityList: %v", err)
		return
	}
	if !hasAllOpenRules(sl) {
		t.Errorf("default seclist not all-open: ingress=%+v egress=%+v", sl.IngressRules, sl.EgressRules)
	}
}

// hasAllOpenRules 判断安全列表出入方向是否均放行 IPv4 与 IPv6 全部流量。
func hasAllOpenRules(sl SecurityList) bool {
	in, out := map[string]bool{}, map[string]bool{}
	for _, r := range sl.IngressRules {
		if r.Protocol == "all" {
			in[r.Source] = true
		}
	}
	for _, r := range sl.EgressRules {
		if r.Protocol == "all" {
			out[r.Destination] = true
		}
	}
	return in["0.0.0.0/0"] && in["::/0"] && out["0.0.0.0/0"] && out["::/0"]
}

// deleteInstanceIpv6AndVerify 取消分配实例的全部 IPv6 地址并确认清空。
func deleteInstanceIpv6AndVerify(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, instanceID string, addrs []string) {
	t.Helper()
	if len(addrs) == 0 {
		return
	}
	for _, addr := range addrs {
		if err := client.DeleteInstanceIpv6(ctx, cred, "", instanceID, addr); err != nil {
			t.Fatalf("DeleteInstanceIpv6 %s: %v", addr, err)
		}
	}
	after, err := client.GetInstance(ctx, cred, "", instanceID)
	if err != nil {
		t.Fatalf("GetInstance after ipv6 delete: %v", err)
	}
	if len(after.Ipv6Addresses) != 0 {
		t.Errorf("ipv6 addresses after delete = %v, want none", after.Ipv6Addresses)
	}
	t.Logf("ipv6 deleted: %v", addrs)
	// 删除 IPv6 后立即终止实例，偶发 OCI 侧 VNIC 级联清理失败回滚
	// （VNIC 残留 AVAILABLE 挡住删子网），等待片刻让状态收敛
	time.Sleep(15 * time.Second)
}

// TestRealOCIVolumeAttachment 验证块卷附加/分离与引导卷挂载查询。
// 依赖已有实例：需先运行上面的测试留存实例，或有其他运行中实例；否则跳过。
// 这里独立创建一个实例做完整链路，A1 容量不足时跳过。
func TestRealOCIVolumeAttachment(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(), 15*time.Minute)
	defer cancel()
	cred := loadTestCredentials(t, "试用期")
	client := NewClient()
	prefix := fmt.Sprintf("oci-portal-e2e-%d", time.Now().Unix())

	ads, err := client.ListAvailabilityDomains(ctx, cred, "")
	if err != nil || len(ads) == 0 {
		t.Fatalf("ListAvailabilityDomains: %v", err)
	}
	images, err := client.ListImages(ctx, cred, ImagesQuery{OperatingSystem: "Canonical Ubuntu", Shape: "VM.Standard.A1.Flex"})
	if err != nil || len(images) == 0 {
		t.Fatalf("ListImages: %v", err)
	}
	vcn, err := client.CreateVCN(ctx, cred, CreateVCNInput{DisplayName: prefix + "-vcn", CidrBlock: "10.97.0.0/16", DnsLabel: "e2evol"})
	if err != nil {
		t.Fatalf("CreateVCN: %v", err)
	}
	defer cleanupVCN(t, client, cred, vcn.ID)
	subnet, err := client.CreateSubnet(ctx, cred, CreateSubnetInput{VcnID: vcn.ID, DisplayName: prefix + "-subnet", CidrBlock: "10.97.1.0/24", DnsLabel: "e2sub"})
	if err != nil {
		t.Fatalf("CreateSubnet: %v", err)
	}
	instance, err := client.LaunchInstance(ctx, cred, CreateInstanceInput{
		AvailabilityDomain: ads[0], DisplayName: prefix + "-vm", Shape: "VM.Standard.A1.Flex",
		Ocpus: 1, MemoryInGBs: 6, ImageID: images[0].ID, BootVolumeSizeGBs: 50, SubnetID: subnet.ID,
	})
	if err != nil {
		if strings.Contains(err.Error(), "capacity") {
			t.Skipf("A1 capacity unavailable: %v", err)
		}
		t.Fatalf("LaunchInstance: %v", err)
	}
	defer terminateAndWait(t, client, cred, instance.ID)
	if err := waitInstanceState(ctx, client, cred, instance.ID, "RUNNING"); err != nil {
		t.Fatalf("wait RUNNING: %v", err)
	}

	// 引导卷挂载查询
	bvAtts, err := client.ListBootVolumeAttachments(ctx, cred, "", instance.ID)
	if err != nil {
		t.Fatalf("ListBootVolumeAttachments: %v", err)
	}
	if len(bvAtts) == 0 {
		t.Error("no boot volume attachment, want one")
	} else {
		t.Logf("boot volume attachment: %s state=%s", bvAtts[0].ID, bvAtts[0].LifecycleState)
	}

	// 创建块卷并附加、分离
	volID := createTestBlockVolume(ctx, t, client, cred, ads[0], prefix)
	if volID == "" {
		return
	}
	defer deleteTestBlockVolume(t, client, cred, volID)

	att, err := client.AttachVolume(ctx, cred, "", instance.ID, volID, false)
	if err != nil {
		t.Fatalf("AttachVolume: %v", err)
	}
	t.Logf("attached volume: %s device=%s", att.ID, att.Device)
	if err := waitVolumeAttachmentState(ctx, client, cred, instance.ID, att.ID, "ATTACHED"); err != nil {
		t.Fatalf("wait volume ATTACHED: %v", err)
	}
	atts, err := client.ListVolumeAttachments(ctx, cred, "", instance.ID)
	if err != nil || len(atts) == 0 {
		t.Fatalf("ListVolumeAttachments: %v (got %d)", err, len(atts))
	}
	if err := client.DetachVolume(ctx, cred, "", att.ID); err != nil {
		t.Fatalf("DetachVolume: %v", err)
	}
	if err := waitVolumeAttachmentState(ctx, client, cred, instance.ID, att.ID, "DETACHED"); err != nil {
		t.Errorf("wait volume DETACHED: %v", err)
	}
	t.Log("volume detached")
}

func createTestBlockVolume(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, ad, prefix string) string {
	t.Helper()
	bc, err := client.blockstorageClient(cred, "")
	if err != nil {
		t.Errorf("blockstorage client: %v", err)
		return ""
	}
	size := int64(50)
	name := prefix + "-vol"
	resp, err := bc.CreateVolume(ctx, createVolumeRequest(cred.TenancyOCID, ad, name, size))
	if err != nil {
		t.Fatalf("CreateVolume: %v", err)
	}
	volID := deref(resp.Id)
	for i := 0; i < 60; i++ {
		got, err := bc.GetVolume(ctx, getVolumeRequest(volID))
		if err == nil && string(got.LifecycleState) == "AVAILABLE" {
			return volID
		}
		select {
		case <-ctx.Done():
			return volID
		case <-time.After(3 * time.Second):
		}
	}
	return volID
}

func waitVolumeAttachmentState(ctx context.Context, client *RealClient, cred Credentials, instanceID, attachmentID, want string) error {
	for i := 0; i < 60; i++ {
		atts, err := client.ListVolumeAttachments(ctx, cred, "", instanceID)
		if err != nil {
			return fmt.Errorf("poll volume attachment: %w", err)
		}
		for _, a := range atts {
			if a.ID == attachmentID && a.LifecycleState == want {
				return nil
			}
		}
		if want == "DETACHED" {
			// 分离后可能不再出现在列表中，视为完成
			found := false
			for _, a := range atts {
				if a.ID == attachmentID {
					found = true
				}
			}
			if !found {
				return nil
			}
		}
		select {
		case <-ctx.Done():
			return fmt.Errorf("poll volume attachment %s: %w", want, ctx.Err())
		case <-time.After(3 * time.Second):
		}
	}
	return fmt.Errorf("poll volume attachment %s: timed out", want)
}

func deleteTestBlockVolume(t *testing.T, client *RealClient, cred Credentials, volID string) {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()
	bc, err := client.blockstorageClient(cred, "")
	if err != nil {
		t.Errorf("cleanup: blockstorage client: %v", err)
		return
	}
	if _, err := bc.DeleteVolume(ctx, deleteVolumeRequest(volID)); err != nil {
		t.Errorf("cleanup: delete volume: %v", err)
		return
	}
	t.Logf("cleanup: volume %s deleted", volID)
}
