package toyota

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"strings"
	"testing"

	"drivervault/apiserver/internal/plugins"
)

func TestDescriptor(t *testing.T) {
	d := (&Plugin{}).Descriptor()
	if d.Name != "toyota" {
		t.Fatalf("name = %q, want toyota", d.Name)
	}
	if d.Kind != plugins.KindBuiltin {
		t.Fatalf("kind = %q, want builtin", d.Kind)
	}
	if len(d.Capabilities) == 0 {
		t.Fatal("expected capabilities")
	}
	// The credential + brand fields must be advertised. They are intentionally
	// NOT required at the global layer — credentials come from the per-user
	// cascade — and the password field must be marked secret.
	fields := map[string]plugins.ConfigField{}
	for _, f := range d.ConfigFields {
		fields[f.Key] = f
	}
	for _, k := range []string{"username", "password", "brand"} {
		if _, ok := fields[k]; !ok {
			t.Errorf("config field %q should be present", k)
		}
	}
	if fields["username"].Required || fields["password"].Required {
		t.Error("credentials must not be required at the global layer (per-user cascade)")
	}
	if !fields["password"].Secret {
		t.Error("password field must be marked secret")
	}
}

// TestEndpointsFollowUpstream guards the mid-2026 migration: Toyota put the
// /v1/global/remote read routes behind AWS SigV4, where a Bearer token is
// answered with 403, and the app moved to /v1/vehicle/*. Falling back to the old
// paths would leave the cards permanently in error.
func TestEndpointsFollowUpstream(t *testing.T) {
	retired := []string{
		"/v1/global/remote/status",
		"/v1/global/remote/climate-status",
		"/v1/global/remote/climate-settings",
	}
	for _, c := range (&Plugin{}).Descriptor().Capabilities {
		for _, dead := range retired {
			if c.Endpoint == dead {
				t.Errorf("capability %q still points at the retired %s", c.ID, dead)
			}
		}
	}
	if epRemoteStatus != "/v1/vehicle/status" {
		t.Errorf("status endpoint = %q, want /v1/vehicle/status", epRemoteStatus)
	}
	// The electric route was not part of that migration and must stay put.
	if epElectricStatus != "/v1/global/remote/electric/status" {
		t.Errorf("electric endpoint = %q, want the unmoved global route", epElectricStatus)
	}
}

// TestActionsCoverCapabilities keeps Invoke and the descriptor in step: every
// advertised capability must be callable, since the car tab drives the plugin by
// the ids the descriptor publishes.
func TestActionsCoverCapabilities(t *testing.T) {
	p := &Plugin{}
	_ = p.Init(context.Background(), map[string]string{"username": "u", "password": "p"})

	for _, c := range p.Descriptor().Capabilities {
		if c.ID == "vehicles" {
			continue // the only action that takes no vin
		}
		// No vin, so the call is rejected before any network I/O — an "unknown
		// action" here means the switch in Invoke was never extended.
		_, err := p.Invoke(context.Background(), c.ID, nil)
		if err == nil {
			t.Errorf("action %q: expected an error without a vin", c.ID)
			continue
		}
		if strings.Contains(err.Error(), "unknown action") {
			t.Errorf("capability %q is advertised but not wired into Invoke", c.ID)
		}
	}
}

func TestRegistered(t *testing.T) {
	// The plugin must self-register via init() so the manager can construct it.
	var found bool
	for _, v := range plugins.NewManager(plugins.NewMemoryStore(nil)).List() {
		if v.Name == "toyota" {
			found = true
		}
	}
	if !found {
		t.Fatal("toyota not registered with the plugin manager")
	}
}

func TestHMACSHA256(t *testing.T) {
	// Mirrors pytoyoda: hmac.new(b"2.14.0", b"abc", sha256).hexdigest().
	got := hmacSHA256("2.14.0", "abc")
	if len(got) != 64 {
		t.Fatalf("hex digest length = %d, want 64", len(got))
	}
	// Deterministic: same key/message → same digest.
	if got != hmacSHA256("2.14.0", "abc") {
		t.Fatal("hmac not deterministic")
	}
	// Key matters: a different client version changes the digest.
	if got == hmacSHA256("9.9.9", "abc") {
		t.Fatal("hmac ignored the key")
	}
}

func TestJWTUUID(t *testing.T) {
	payload := base64.RawURLEncoding.EncodeToString([]byte(`{"uuid":"abc-123","aud":"oneappsdkclient"}`))
	token := "header." + payload + ".sig"
	uuid, err := jwtUUID(token)
	if err != nil {
		t.Fatalf("jwtUUID: %v", err)
	}
	if uuid != "abc-123" {
		t.Fatalf("uuid = %q, want abc-123", uuid)
	}

	if _, err := jwtUUID("not-a-jwt"); err == nil {
		t.Error("expected error for malformed token")
	}
}

func TestExtractCode(t *testing.T) {
	loc := "com.toyota.oneapp:/oauth2Callback?code=AUTHCODE123&state=xyz"
	if got := extractCode(loc); got != "AUTHCODE123" {
		t.Fatalf("code = %q, want AUTHCODE123", got)
	}
	if got := extractCode("com.toyota.oneapp:/oauth2Callback"); got != "" {
		t.Fatalf("expected empty code, got %q", got)
	}
}

func TestCallbackHelpers(t *testing.T) {
	// A NameCallback shaped like ForgeRock's response.
	var cb map[string]any
	_ = json.Unmarshal([]byte(`{
		"type":"NameCallback",
		"output":[{"name":"prompt","value":"User Name"}],
		"input":[{"name":"IDToken1","value":""}]
	}`), &cb)

	if output0(cb) != "User Name" {
		t.Fatalf("output0 = %q", output0(cb))
	}
	setInput0(cb, "driver@example.com")
	in := cb["input"].([]any)[0].(map[string]any)
	if in["value"] != "driver@example.com" {
		t.Fatalf("input value = %v, want driver@example.com", in["value"])
	}
}

func TestInvokeRequiresVIN(t *testing.T) {
	p := &Plugin{}
	_ = p.Init(context.Background(), map[string]string{"username": "u", "password": "p"})

	if _, err := p.Invoke(context.Background(), "telemetry", nil); err == nil ||
		!strings.Contains(err.Error(), "requires a vin") {
		t.Fatalf("expected vin-required error, got %v", 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)
	}
}
