package api

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// refuse runs one action through the cloud transport and returns the response.
// Only the actions refused up front reach the plugin-free path, which is exactly
// what these cases cover.
func refuse(t *testing.T, action string, body ankerControlBody) *httptest.ResponseRecorder {
	t.Helper()
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPost, "/api/integrations/anker-solix/control/SN1/"+action, nil)
	(&Server{}).ankerMqttAction(rec, req, nil, ankerResolution{}, "SN1", action, body)
	return rec
}

// An action this transport cannot send must say which transport can, so the
// answer tells the operator what to change rather than that something is
// unknown.
func TestAnkerMqttActionNamesTheTransportThatCan(t *testing.T) {
	for _, tc := range []struct{ action, want string }{
		{"unlock", "CSMS"},
		{"availability", "CSMS"},
		{"config", "CSMS"},
		{"phase", "Modbus TCP"},
		{"timeout", "Modbus TCP"},
		{"clear-limit", "with the amps you want"},
	} {
		rec := refuse(t, tc.action, ankerControlBody{})
		if rec.Code != http.StatusBadRequest {
			t.Errorf("%s returned %d, want 400", tc.action, rec.Code)
			continue
		}
		if !strings.Contains(rec.Body.String(), tc.want) {
			t.Errorf("%s answered %q, want it to mention %q", tc.action, rec.Body.String(), tc.want)
		}
	}
}

// A reboot is a reboot under either name and over any transport: both go through
// the confirmation and the password step-up, and nothing else does.
func TestBothNamesForARebootAreGated(t *testing.T) {
	for _, action := range []string{"reset", "restart", "unlock"} {
		if !isDestructiveAction(action) {
			t.Errorf("%s should need a confirmation and a password", action)
		}
	}
	for _, action := range []string{"start", "stop", "limit", "boost", "settings", "status"} {
		if isDestructiveAction(action) {
			t.Errorf("%s should not demand a password", action)
		}
	}
}

func TestAnkerMqttActionRejectsUnknownActions(t *testing.T) {
	rec := refuse(t, "explode", ankerControlBody{})
	if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "unknown control action") {
		t.Errorf("unknown action returned %d %q", rec.Code, rec.Body.String())
	}
}

// Boost runs until the session ends and there is no message to cancel it, so
// asking to switch it off has to be refused rather than quietly sending a boost.
func TestAnkerMqttActionRefusesTurningBoostOff(t *testing.T) {
	off := false
	rec := refuse(t, "boost", ankerControlBody{On: &off})
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("boost off returned %d, want 400", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "ends with the charging session") {
		t.Errorf("boost off answered %q, want it to explain when boost ends", rec.Body.String())
	}
}

// A settings write with nothing to write is a mistake worth naming, not an empty
// command published to the charger.
func TestAnkerMqttActionRequiresSettingsToWrite(t *testing.T) {
	rec := refuse(t, "settings", ankerControlBody{})
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("empty settings returned %d, want 400", rec.Code)
	}
	if !strings.Contains(rec.Body.String(), "settings") {
		t.Errorf("empty settings answered %q, want it to name the field it wants", rec.Body.String())
	}
}

// The cloud transport signs in as the account, so unlike Modbus it carries the
// caller's resolved credentials into the plugin call.
func TestAnkerCloudConfigCarriesTheResolvedCredentials(t *testing.T) {
	cfg := ankerCloudConfig(ankerResolution{
		eff: ankerConfig{Email: "a@example.com", Password: "secret", Country: "DK"},
	})
	if cfg["email"] != "a@example.com" || cfg["password"] != "secret" || cfg["country"] != "DK" {
		t.Errorf("config = %v, want the resolved credentials", cfg)
	}
	if _, ok := cfg["controlMode"]; ok {
		t.Error("the control mode is the API server's business, not the plugin's")
	}
}

func TestMustJSONEncodesTheCommandPayload(t *testing.T) {
	var got map[string]any
	if err := json.Unmarshal(mustJSON(map[string]any{"sn": "SN1", "command": "limit", "amps": 16.0}), &got); err != nil {
		t.Fatalf("mustJSON produced invalid JSON: %v", err)
	}
	if got["sn"] != "SN1" || got["command"] != "limit" || got["amps"] != 16.0 {
		t.Errorf("payload = %v", got)
	}
}

func TestShortenDetailKeepsTheAnswerReadable(t *testing.T) {
	if got := shortenDetail("  broke\nbadly  "); got != "broke badly" {
		t.Errorf("shortenDetail = %q", got)
	}
	if got := shortenDetail(""); got != "no detail" {
		t.Errorf("shortenDetail of nothing = %q", got)
	}
	long := shortenDetail(strings.Repeat("x", 400))
	if len([]rune(long)) != 201 || !strings.HasSuffix(long, "…") {
		t.Errorf("a long detail was not trimmed: %d runes", len([]rune(long)))
	}
}
