package ankersolix

import (
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/ecdh"
	"crypto/md5"
	"crypto/rand"
	"encoding/base64"
	"encoding/hex"
	"regexp"
	"strings"
	"testing"

	"drivervault/apiserver/internal/plugins"
)

func TestDescriptor(t *testing.T) {
	d := (&Plugin{}).Descriptor()
	if d.Name != "anker-solix" {
		t.Fatalf("name = %q, want anker-solix", d.Name)
	}
	if d.Kind != plugins.KindBuiltin {
		t.Fatalf("kind = %q, want builtin", d.Kind)
	}
	if len(d.Capabilities) == 0 {
		t.Fatal("expected capabilities")
	}
	fields := map[string]plugins.ConfigField{}
	for _, f := range d.ConfigFields {
		fields[f.Key] = f
	}
	for _, k := range []string{"email", "password", "country"} {
		if _, ok := fields[k]; !ok {
			t.Errorf("config field %q should be present", k)
		}
	}
	if fields["email"].Required || fields["password"].Required {
		t.Error("credentials must not be required at the global layer")
	}
	if !fields["password"].Secret {
		t.Error("password field must be marked secret")
	}

	// controlMode is a select advertising the three OCPP control paths.
	cm, ok := fields["controlMode"]
	if !ok {
		t.Fatal("controlMode config field should be present")
	}
	if cm.Type != "select" {
		t.Errorf("controlMode type = %q, want select", cm.Type)
	}
	if cm.Default != "off" {
		t.Errorf("controlMode default = %q, want off", cm.Default)
	}
	want := map[string]bool{"off": false, "own": false, "proxy": false}
	for _, o := range cm.Options {
		if _, known := want[o.Value]; known {
			want[o.Value] = true
		}
	}
	for v, seen := range want {
		if !seen {
			t.Errorf("controlMode is missing option %q", v)
		}
	}
}

func TestRegistered(t *testing.T) {
	var found bool
	for _, v := range plugins.NewManager(plugins.NewMemoryStore(nil)).List() {
		if v.Name == "anker-solix" {
			found = true
		}
	}
	if !found {
		t.Fatal("anker-solix not registered with the plugin manager")
	}
}

func TestServerSelection(t *testing.T) {
	cases := map[string]string{
		"DE": serverEU,
		"GB": serverEU, // not in the COM list → EU default
		"US": serverCOM,
		"AU": serverCOM,
		"":   serverEU, // empty → defaults to DE → EU
	}
	for country, want := range cases {
		p := &Plugin{}
		_ = p.Init(context.Background(), map[string]string{"country": country})
		if p.apiBase != want {
			t.Errorf("country %q: apiBase = %q, want %q", country, p.apiBase, want)
		}
	}
}

func TestEncryptPasswordRoundTrip(t *testing.T) {
	// The shared secret is 32 bytes; the reference encrypts AES-256-CBC with the
	// key as its own IV[:16] and PKCS#7 padding, base64-encoded. Verify our
	// output decrypts back to the plaintext under the same scheme.
	shared := make([]byte, 32)
	if _, err := rand.Read(shared); err != nil {
		t.Fatal(err)
	}
	const pw = "s3cr3t-pässwörd!"

	enc, err := encryptPassword(pw, shared)
	if err != nil {
		t.Fatalf("encryptPassword: %v", err)
	}
	raw, err := base64.StdEncoding.DecodeString(enc)
	if err != nil {
		t.Fatalf("base64 decode: %v", err)
	}
	if len(raw)%aes.BlockSize != 0 || len(raw) == 0 {
		t.Fatalf("ciphertext length %d not a positive multiple of block size", len(raw))
	}

	block, _ := aes.NewCipher(shared)
	out := make([]byte, len(raw))
	cipher.NewCBCDecrypter(block, shared[:aes.BlockSize]).CryptBlocks(out, raw)
	// strip PKCS#7 padding
	pad := int(out[len(out)-1])
	if pad < 1 || pad > aes.BlockSize {
		t.Fatalf("bad padding byte %d", pad)
	}
	if got := string(out[:len(out)-pad]); got != pw {
		t.Fatalf("round-trip = %q, want %q", got, pw)
	}
}

func TestEncryptPasswordShortKey(t *testing.T) {
	if _, err := encryptPassword("x", make([]byte, 16)); err == nil {
		t.Fatal("expected error for a too-short shared key")
	}
}

func TestMD5Hex(t *testing.T) {
	// gtoken = md5(user_id). Verify against the standard library.
	want := md5.Sum([]byte("user-123"))
	if got := md5hex("user-123"); got != hex.EncodeToString(want[:]) {
		t.Fatalf("md5hex = %q, want %q", got, hex.EncodeToString(want[:]))
	}
	if len(md5hex("anything")) != 32 {
		t.Fatal("md5 hex digest must be 32 chars")
	}
}

func TestClientPublicKeyFormat(t *testing.T) {
	// The client public key sent to Anker is the uncompressed P-256 point
	// (0x04 || X || Y = 65 bytes) in hex, and Anker's server key must parse.
	priv, err := ecdh.P256().GenerateKey(rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	pub := priv.PublicKey().Bytes()
	if len(pub) != 65 || pub[0] != 0x04 {
		t.Fatalf("client public key not uncompressed 65-byte point: len=%d first=0x%02x", len(pub), pub[0])
	}

	serverBytes, err := hex.DecodeString(serverPublicKeyHex)
	if err != nil {
		t.Fatalf("server key hex: %v", err)
	}
	if _, err := ecdh.P256().NewPublicKey(serverBytes); err != nil {
		t.Fatalf("server public key does not parse as a P-256 point: %v", err)
	}
}

func TestTimezoneFormat(t *testing.T) {
	gmt, ms := timezone()
	if !regexp.MustCompile(`^GMT[+-]\d{2}:\d{2}$`).MatchString(gmt) {
		t.Fatalf("timezone string %q does not match GMT+HH:MM", gmt)
	}
	// The millisecond offset must be consistent with the string's whole hours.
	if (ms/1000)%60 != 0 && strings.HasSuffix(gmt, ":00") {
		t.Fatalf("offset ms %d inconsistent with %q", ms, gmt)
	}
}

func TestInvokeRequiresSN(t *testing.T) {
	p := &Plugin{}
	_ = p.Init(context.Background(), map[string]string{"email": "u@example.com", "password": "p"})

	for _, action := range []string{"charger-status", "charge-stats", "charge-orders", "ocpp-info"} {
		if _, err := p.Invoke(context.Background(), action, nil); err == nil ||
			!strings.Contains(err.Error(), "requires an sn") {
			t.Fatalf("action %q: expected sn-required error, got %v", action, err)
		}
	}
	if _, err := p.Invoke(context.Background(), "bogus", nil); err == nil ||
		!strings.Contains(err.Error(), "unknown action") {
		t.Fatalf("expected unknown-action error, got %v", err)
	}
}

func TestCountChargers(t *testing.T) {
	body := []byte(`{"code":0,"msg":"success!","data":{"evChargers":[],"userBindEvChargersCount":2}}`)
	if n, ok := countChargers(body); !ok || n != 2 {
		t.Fatalf("countChargers = (%d,%v), want (2,true)", n, ok)
	}
	if _, ok := countChargers([]byte("not json")); ok {
		t.Fatal("expected ok=false for invalid json")
	}
}

func TestApiErrorAndAuthCode(t *testing.T) {
	if _, _, ok := apiError([]byte(`{"code":0,"msg":"success!"}`)); ok {
		t.Fatal("code 0 must not be an error")
	}
	code, msg, ok := apiError([]byte(`{"code":10000,"msg":"boom"}`))
	if !ok || code != 10000 || msg != "boom" {
		t.Fatalf("apiError = (%d,%q,%v)", code, msg, ok)
	}
	if !isAuthCode([]byte(`{"code":401,"msg":"token invalid"}`)) {
		t.Fatal("401 should be an auth code")
	}
	if isAuthCode([]byte(`{"code":10000,"msg":"other"}`)) {
		t.Fatal("10000 should not be an auth code")
	}
}

// TestLoginBodyShape guards the exact login payload the reference builds, without
// hitting the network: it re-derives the encrypted password and confirms it
// decrypts under the same ECDH shared secret. (Sanity check on the wiring.)
func TestLoginBodyShape(t *testing.T) {
	priv, _ := ecdh.P256().GenerateKey(rand.Reader)
	serverBytes, _ := hex.DecodeString(serverPublicKeyHex)
	serverPub, _ := ecdh.P256().NewPublicKey(serverBytes)
	shared, err := priv.ECDH(serverPub)
	if err != nil {
		t.Fatal(err)
	}
	if len(shared) != 32 {
		t.Fatalf("shared secret len = %d, want 32", len(shared))
	}
	enc, err := encryptPassword("pw", shared)
	if err != nil {
		t.Fatal(err)
	}
	raw, _ := base64.StdEncoding.DecodeString(enc)
	block, _ := aes.NewCipher(shared)
	out := make([]byte, len(raw))
	cipher.NewCBCDecrypter(block, shared[:aes.BlockSize]).CryptBlocks(out, raw)
	pad := int(out[len(out)-1])
	if !bytes.Equal(out[:len(out)-pad], []byte("pw")) {
		t.Fatal("login password encryption did not round-trip")
	}
}

func TestInvokeRequiresSiteID(t *testing.T) {
	p := &Plugin{}
	_ = p.Init(context.Background(), map[string]string{"email": "u@example.com", "password": "p"})

	for _, action := range []string{"charger-state", "site-status", "charge-energy"} {
		if _, err := p.Invoke(context.Background(), action, nil); err == nil ||
			!strings.Contains(err.Error(), "requires a siteId") {
			t.Fatalf("action %q: expected siteId-required error, got %v", action, err)
		}
	}
	if _, err := p.Invoke(context.Background(), "vehicle", nil); err == nil ||
		!strings.Contains(err.Error(), "requires a vehicleId") {
		t.Fatalf("expected vehicleId-required error, got %v", err)
	}
}

func TestEnergyRange(t *testing.T) {
	for in, want := range map[string]string{
		"day": "day", "week": "week", "month": "month", "year": "year",
		"YEAR": "year", " month ": "month",
		"": "week", "decade": "week",
	} {
		if got := energyRange(in); got != want {
			t.Errorf("energyRange(%q) = %q, want %q", in, got, want)
		}
	}
}

func TestChargerMode(t *testing.T) {
	// Mirrors anker-solix-api's ev_charger_mode_state: a boost overrides
	// everything, preparing splits on the countdowns, and the three active
	// charging states all read as start_charge.
	cases := []struct {
		status                        string
		boost                         bool
		plugCountdown, startCountdown int
		want                          string
	}{
		{stateCharging, true, 0, 0, modeBoostCharge},
		{stateStandby, true, 0, 0, modeBoostCharge},
		{statePreparing, false, 30, 0, modeWaitPlug},
		{statePreparing, false, 0, 30, modeWaitStart},
		{statePreparing, false, 30, 30, modeWaitPlug}, // plug wins over start
		{statePreparing, false, 0, 0, modeStartCharge},
		{stateCharging, false, 0, 0, modeStartCharge},
		{stateChargerPaused, false, 0, 0, modeStartCharge},
		{stateVehiclePaused, false, 0, 0, modeStartCharge},
		{stateStandby, false, 0, 0, modeStopCharge},
		{stateCompleted, false, 0, 0, modeStopCharge},
		{stateDisabled, false, 0, 0, modeStopCharge},
		{stateError, false, 0, 0, modeStopCharge},
		{stateUnknown, false, 0, 0, modeStopCharge},
	}
	for _, c := range cases {
		got := chargerMode(c.status, c.boost, c.plugCountdown, c.startCountdown)
		if got != c.want {
			t.Errorf("chargerMode(%q, boost=%v, %d, %d) = %q, want %q",
				c.status, c.boost, c.plugCountdown, c.startCountdown, got, c.want)
		}
	}
}

func TestChargerModeOptions(t *testing.T) {
	// Mirrors ev_charger_mode_options; results are sorted.
	cases := []struct {
		mode, status string
		want         []string
	}{
		{modeStartCharge, stateCharging, []string{modeBoostCharge, modeStartCharge, modeStopCharge}},
		{modeWaitStart, statePreparing, []string{modeSkipDelay, modeStopCharge, modeWaitStart}},
		{modeWaitPlug, statePreparing, []string{modeStopCharge, modeWaitPlug}},
		{modeBoostCharge, stateCharging, []string{modeBoostCharge, modeStopCharge}},
		// Stopped: startable only from standby.
		{modeStopCharge, stateStandby, []string{modeStartCharge, modeStopCharge}},
		{modeStopCharge, stateCompleted, []string{modeStopCharge}},
		{modeStopCharge, stateError, []string{modeStopCharge}},
		{"", stateUnknown, []string{}},
	}
	for _, c := range cases {
		got := chargerModeOptions(c.mode, c.status)
		if len(got) != len(c.want) {
			t.Errorf("chargerModeOptions(%q, %q) = %v, want %v", c.mode, c.status, got, c.want)
			continue
		}
		for i := range got {
			if got[i] != c.want[i] {
				t.Errorf("chargerModeOptions(%q, %q) = %v, want %v", c.mode, c.status, got, c.want)
				break
			}
		}
	}
}

func TestParseScenePiles(t *testing.T) {
	body := []byte(`{"code":0,"data":{"charging_pile_info":{"charging_pile_list":[
		{"device_sn":"EVSN1","device_name":"Garage","operating_state":2,"power":"7.4","ocpp_connect_status":2},
		{"device_sn":"","device_name":"nameless"},
		{"device_sn":"EVSN2","operating_state":99}]}}}`)

	got := parseScenePiles(body, "site-1")
	if len(got) != 2 {
		t.Fatalf("parsed %d chargers, want 2 (the SN-less entry is dropped)", len(got))
	}

	c := got[0]
	if c.SN != "EVSN1" || c.Name != "Garage" || c.SiteID != "site-1" || c.Source != "scene" {
		t.Errorf("identity = %+v", c)
	}
	if c.StatusDesc != stateCharging || c.Mode != modeStartCharge {
		t.Errorf("status/mode = %q/%q, want %q/%q", c.StatusDesc, c.Mode, stateCharging, modeStartCharge)
	}
	if c.Charging == nil || !*c.Charging {
		t.Error("a charging charger must read as charging")
	}
	if c.Power != "7.4" {
		t.Errorf("power = %q, want 7.4", c.Power)
	}
	if c.OcppStatusDesc != "connected" {
		t.Errorf("ocppStatusDesc = %q, want connected", c.OcppStatusDesc)
	}

	// An unmapped status code degrades to unknown, and unknown stops.
	if got[1].StatusDesc != stateUnknown || got[1].Mode != modeStopCharge {
		t.Errorf("unmapped status = %q/%q, want %q/%q",
			got[1].StatusDesc, got[1].Mode, stateUnknown, modeStopCharge)
	}
	if got[1].OcppStatus != nil {
		t.Error("absent ocpp_connect_status must stay nil, not default to disconnected")
	}
	if parseScenePiles([]byte("not json"), "site-1") != nil {
		t.Error("invalid json should parse to nil")
	}
}

func TestParseHesChargers(t *testing.T) {
	body := []byte(`{"code":0,"data":{"hasEvCharger":true,"evChargerInfos":[
		{"evChargerSn":"EVSN9","evChargerStatus":0,"evChargerName":"V1 EV Charger"}]}}`)

	got := parseHesChargers(body, "site-2")
	if len(got) != 1 {
		t.Fatalf("parsed %d chargers, want 1", len(got))
	}
	c := got[0]
	if c.SN != "EVSN9" || c.Source != "hes" || c.SiteID != "site-2" {
		t.Errorf("identity = %+v", c)
	}
	if c.StatusDesc != stateStandby || c.Mode != modeStopCharge {
		t.Errorf("status/mode = %q/%q, want %q/%q", c.StatusDesc, c.Mode, stateStandby, modeStopCharge)
	}
	if c.Charging == nil || *c.Charging {
		t.Error("a standby charger must not read as charging")
	}
	// Standby is the one stopped state that can be started again.
	var startable bool
	for _, o := range c.ModeOptions {
		if o == modeStartCharge {
			startable = true
		}
	}
	if !startable {
		t.Errorf("standby should offer start_charge, options = %v", c.ModeOptions)
	}
	if parseHesChargers([]byte("{"), "site-2") != nil {
		t.Error("invalid json should parse to nil")
	}
}

func TestStatusName(t *testing.T) {
	for code, want := range map[int]string{
		0: stateStandby, 1: statePreparing, 2: stateCharging, 3: stateChargerPaused,
		4: stateVehiclePaused, 5: stateCompleted, 6: stateReserving, 7: stateDisabled,
		8: stateError, 9: stateUnknown, -1: stateUnknown,
	} {
		if got := statusName(code); got != want {
			t.Errorf("statusName(%d) = %q, want %q", code, got, want)
		}
	}
}
