package service

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"

	"oci-portal/internal/crypto"
	"oci-portal/internal/model"
	"oci-portal/internal/oci"
)

func newProxyEnv(t *testing.T) (*ProxyService, *gorm.DB) {
	t.Helper()
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}
	sqlDB, _ := db.DB()
	sqlDB.SetMaxOpenConns(1)
	if err := db.AutoMigrate(&model.Proxy{}, &model.OciConfig{}); err != nil {
		t.Fatalf("migrate: %v", err)
	}
	cipher, err := crypto.NewCipher("test-key")
	if err != nil {
		t.Fatalf("cipher: %v", err)
	}
	svc := NewProxyService(db, cipher)
	// 默认注入空 client:异步探测直接跳过,避免测试外呼与写库竞态
	svc.geoClientFor = func(*oci.ProxySpec) *http.Client { return nil }
	t.Cleanup(svc.Wait)
	return svc, db
}

func strp(s string) *string { return &s }

func TestProxyCRUDAndPasswordSemantics(t *testing.T) {
	svc, db := newProxyEnv(t)
	ctx := context.Background()

	created, err := svc.Create(ctx, ProxyInput{Name: "jp-socks", Type: "socks5", Host: "1.2.3.4", Port: 1080, Username: "u", Password: strp("secret")})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if !created.PasswordSet {
		t.Fatalf("PasswordSet = false, want true")
	}
	var row model.Proxy
	if err := db.First(&row, created.ID).Error; err != nil {
		t.Fatalf("load row: %v", err)
	}
	if row.PasswordEnc == "" || row.PasswordEnc == "secret" {
		t.Fatalf("password must be stored encrypted, got %q", row.PasswordEnc)
	}

	// 密码缺省沿用
	updated, err := svc.Update(ctx, created.ID, ProxyInput{Name: "jp-socks", Type: "socks5", Host: "1.2.3.4", Port: 1081})
	if err != nil {
		t.Fatalf("update: %v", err)
	}
	if !updated.PasswordSet || updated.Port != 1081 {
		t.Fatalf("update view = %+v, want password kept and port 1081", updated)
	}
	// 空串清除
	cleared, err := svc.Update(ctx, created.ID, ProxyInput{Name: "jp-socks", Type: "socks5", Host: "1.2.3.4", Port: 1081, Password: strp("")})
	if err != nil {
		t.Fatalf("update clear: %v", err)
	}
	if cleared.PasswordSet {
		t.Fatalf("PasswordSet = true after clear, want false")
	}

	spec, err := svc.SpecOf(ctx, &created.ID)
	if err != nil {
		t.Fatalf("SpecOf: %v", err)
	}
	if spec == nil || spec.Host != "1.2.3.4" || spec.Port != 1081 {
		t.Fatalf("spec = %+v", spec)
	}
	if spec2, err := svc.SpecOf(ctx, nil); err != nil || spec2 != nil {
		t.Fatalf("SpecOf(nil) = %+v, %v; want nil, nil", spec2, err)
	}

	if err := svc.Delete(ctx, created.ID); err != nil {
		t.Fatalf("delete: %v", err)
	}
}

func TestProxyValidateAndDeleteInUse(t *testing.T) {
	svc, db := newProxyEnv(t)
	ctx := context.Background()

	for _, in := range []ProxyInput{
		{Name: "x", Type: "ss", Host: "h", Port: 1080},
		{Name: "x", Type: "http", Host: "h", Port: 0},
		{Name: "x", Type: "http", Host: " ", Port: 8080},
	} {
		if _, err := svc.Create(ctx, in); err == nil {
			t.Fatalf("Create(%+v) accepted invalid input", in)
		}
	}

	created, err := svc.Create(ctx, ProxyInput{Name: "used", Type: "http", Host: "h", Port: 8080})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	cfg := model.OciConfig{Alias: "t1", ProxyID: &created.ID}
	if err := db.Create(&cfg).Error; err != nil {
		t.Fatalf("create cfg: %v", err)
	}
	if err := svc.Delete(ctx, created.ID); err == nil {
		t.Fatalf("Delete allowed while in use, want ErrProxyInUse")
	}
	views, err := svc.List(ctx)
	if err != nil || len(views) != 1 || views[0].UsedBy != 1 {
		t.Fatalf("List = %+v, %v; want one proxy with UsedBy=1", views, err)
	}
}

func TestProxyAutoNameAndImport(t *testing.T) {
	svc, _ := newProxyEnv(t)
	ctx := context.Background()

	// 名称缺省自动生成;同 host:port 再建追加序号
	v1, err := svc.Create(ctx, ProxyInput{Type: "socks5", Host: "1.2.3.4", Port: 1080})
	if err != nil || v1.Name != "socks5-1.2.3.4:1080" {
		t.Fatalf("auto name = %q, %v; want socks5-1.2.3.4:1080", v1.Name, err)
	}
	v2, err := svc.Create(ctx, ProxyInput{Type: "socks5", Host: "1.2.3.4", Port: 1080})
	if err != nil || v2.Name != "socks5-1.2.3.4:1080-2" {
		t.Fatalf("auto name #2 = %q, %v; want socks5-1.2.3.4:1080-2", v2.Name, err)
	}

	text := "# 注释与空行跳过\n\n" +
		"socks5://u1:p1@9.9.9.9:1080\n" +
		"https://8.8.8.8:8443\n" +
		"7.7.7.7:1080:u2:p2\n" +
		"badline\n"
	res, err := svc.Import(ctx, text)
	if err != nil {
		t.Fatalf("import: %v", err)
	}
	if len(res.Created) != 3 || len(res.Failed) != 1 {
		t.Fatalf("import = %d created / %d failed, want 3 / 1: %+v", len(res.Created), len(res.Failed), res)
	}
	if res.Created[0].Type != "socks5" || !res.Created[0].PasswordSet || res.Created[0].Username != "u1" {
		t.Fatalf("URI 行解析 = %+v, want socks5 带凭据", res.Created[0])
	}
	if res.Created[1].Type != "https" || res.Created[1].Port != 8443 {
		t.Fatalf("https 行解析 = %+v", res.Created[1])
	}
	if res.Created[2].Type != "socks5" || res.Created[2].Username != "u2" || !res.Created[2].PasswordSet {
		t.Fatalf("colon 行解析 = %+v, want socks5 带凭据", res.Created[2])
	}
	if res.Failed[0].LineNo != 6 || res.Failed[0].Reason == "" {
		t.Fatalf("failed 行 = %+v, want lineNo=6 带原因", res.Failed[0])
	}
}

func TestProxyImportMasksCredentials(t *testing.T) {
	svc, _ := newProxyEnv(t)
	ctx := context.Background()

	// 端口非法的行会失败并回显:凭据段必须已脱敏
	res, err := svc.Import(ctx, "socks5://user:topsecret@1.2.3.4:notaport\n1.2.3.4:notaport:user:topsecret")
	if err != nil || len(res.Failed) != 2 {
		t.Fatalf("import = %+v, %v; want 2 failed", res, err)
	}
	for _, f := range res.Failed {
		if strings.Contains(f.Line, "topsecret") {
			t.Fatalf("failed line %q 泄露密码", f.Line)
		}
		if !strings.Contains(f.Line, "***") {
			t.Fatalf("failed line %q 未打码", f.Line)
		}
	}
}

func TestProxyGeoProbe(t *testing.T) {
	svc, db := newProxyEnv(t)
	ctx := context.Background()

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/ok" {
			_, _ = w.Write([]byte(`{"status":"success","country":"日本","city":"大阪市"}`))
			return
		}
		w.WriteHeader(http.StatusNotFound)
	}))
	defer srv.Close()

	orig := geoEndpoints
	defer func() { geoEndpoints = orig }()
	// 首个端点失败,验证 fallback 到第二个
	geoEndpoints = []geoEndpoint{
		{url: srv.URL + "/fail", parse: parseIPAPI},
		{url: srv.URL + "/ok", parse: parseIPAPI},
	}
	svc.geoClientFor = func(*oci.ProxySpec) *http.Client { return srv.Client() }

	created, err := svc.Create(ctx, ProxyInput{Type: "socks5", Host: "1.2.3.4", Port: 1080})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	svc.Wait()
	var row model.Proxy
	if err := db.First(&row, created.ID).Error; err != nil {
		t.Fatalf("load row: %v", err)
	}
	if row.Country != "日本" || row.City != "大阪市" || row.GeoAt == nil {
		t.Fatalf("geo = %q/%q geoAt=%v, want 日本/大阪市 带时间", row.Country, row.City, row.GeoAt)
	}

	// 全部端点失败:Probe 写空值+时间(未知),不残留旧地区
	geoEndpoints = []geoEndpoint{{url: srv.URL + "/fail", parse: parseIPAPI}}
	view, err := svc.Probe(ctx, created.ID)
	if err != nil {
		t.Fatalf("probe: %v", err)
	}
	if view.Country != "" || view.City != "" || view.GeoAt == "" {
		t.Fatalf("probe view = %+v, want 空地区带探测时间", view)
	}
}

func TestProxySetTenants(t *testing.T) {
	svc, db := newProxyEnv(t)
	ctx := context.Background()

	pa, _ := svc.Create(ctx, ProxyInput{Type: "socks5", Host: "10.0.0.1", Port: 1080})
	pb, _ := svc.Create(ctx, ProxyInput{Type: "socks5", Host: "10.0.0.2", Port: 1080})
	// t1 已挂 A,t2 挂 B,t3 直连
	t1 := model.OciConfig{Alias: "t1", ProxyID: &pa.ID}
	t2 := model.OciConfig{Alias: "t2", ProxyID: &pb.ID}
	t3 := model.OciConfig{Alias: "t3"}
	for _, cfg := range []*model.OciConfig{&t1, &t2, &t3} {
		if err := db.Create(cfg).Error; err != nil {
			t.Fatalf("seed config: %v", err)
		}
	}

	proxyOf := func(id uint) *uint {
		var row model.OciConfig
		if err := db.First(&row, id).Error; err != nil {
			t.Fatalf("load config %d: %v", id, err)
		}
		return row.ProxyID
	}

	cases := []struct {
		name    string
		proxyID uint
		cfgIDs  []uint
		wantN   int64
		wantErr error
	}{
		{name: "改挂并新增", proxyID: pa.ID, cfgIDs: []uint{t2.ID, t3.ID, t3.ID}, wantN: 2},
		{name: "清空解除全部", proxyID: pa.ID, cfgIDs: nil, wantN: 0},
		{name: "无效租户拒绝", proxyID: pa.ID, cfgIDs: []uint{9999}, wantErr: ErrProxyInvalid},
		{name: "代理不存在", proxyID: 9999, cfgIDs: nil, wantErr: gorm.ErrRecordNotFound},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			n, err := svc.SetTenants(ctx, tc.proxyID, tc.cfgIDs)
			if tc.wantErr != nil {
				if !errors.Is(err, tc.wantErr) {
					t.Fatalf("err = %v, want %v", err, tc.wantErr)
				}
				return
			}
			if err != nil || n != tc.wantN {
				t.Fatalf("SetTenants = %d, %v; want %d, nil", n, err, tc.wantN)
			}
		})
	}

	// 终态:清空后三租户全部直连(t1 在第一步已被解除,t2/t3 在第二步解除)
	for _, id := range []uint{t1.ID, t2.ID, t3.ID} {
		if got := proxyOf(id); got != nil {
			t.Fatalf("config %d proxyID = %v, want nil", id, *got)
		}
	}
}

func TestProxySetTenantsRebind(t *testing.T) {
	svc, db := newProxyEnv(t)
	ctx := context.Background()

	pa, _ := svc.Create(ctx, ProxyInput{Type: "socks5", Host: "10.0.1.1", Port: 1080})
	pb, _ := svc.Create(ctx, ProxyInput{Type: "socks5", Host: "10.0.1.2", Port: 1080})
	cfg := model.OciConfig{Alias: "steal", ProxyID: &pa.ID}
	if err := db.Create(&cfg).Error; err != nil {
		t.Fatalf("seed config: %v", err)
	}

	if _, err := svc.SetTenants(ctx, pb.ID, []uint{cfg.ID}); err != nil {
		t.Fatalf("SetTenants: %v", err)
	}
	var row model.OciConfig
	if err := db.First(&row, cfg.ID).Error; err != nil {
		t.Fatalf("load config: %v", err)
	}
	if row.ProxyID == nil || *row.ProxyID != pb.ID {
		t.Fatalf("proxyID = %v, want %d(改挂到 B)", row.ProxyID, pb.ID)
	}
	// A 侧计数应归零,B 侧为 1
	views, err := svc.List(ctx)
	if err != nil {
		t.Fatalf("List: %v", err)
	}
	for _, v := range views {
		want := int64(0)
		if v.ID == pb.ID {
			want = 1
		}
		if v.UsedBy != want {
			t.Fatalf("proxy %d UsedBy = %d, want %d", v.ID, v.UsedBy, want)
		}
	}
}
