package ocpp

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"path"
	"testing"
	"time"
)

// ownModeServer stands up a CSMS behind an httptest server that upgrades every
// /ocpp/{serial} connection and registers it in own mode.
func ownModeServer(t *testing.T) (*CSMS, *httptest.Server, <-chan *Session) {
	t.Helper()
	csms := NewCSMS(nil)
	sessCh := make(chan *Session, 1)
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		conn, err := Upgrade(w, r)
		if err != nil {
			t.Errorf("upgrade: %v", err)
			return
		}
		sess, err := csms.Accept(path.Base(r.URL.Path), ModeOwn, conn, nil)
		if err != nil {
			t.Errorf("accept: %v", err)
			_ = conn.Close()
			return
		}
		sessCh <- sess
	}))
	return csms, srv, sessCh
}

func TestOwnModeEndToEnd(t *testing.T) {
	csms, srv, sessCh := ownModeServer(t)
	defer srv.Close()

	cp := dialSimCP(t, wsURL(srv.URL, "/ocpp/CP-A5191"), nil)
	defer cp.close()
	ctx := context.Background()

	// Boot: the CSMS accepts and the charger appears connected.
	boot := cp.call(t, "BootNotification", map[string]any{
		"chargePointVendor": "Anker", "chargePointModel": "A5191", "firmwareVersion": "1.2.3",
	})
	assertStatus(t, boot, "Accepted")

	var sess *Session
	select {
	case sess = <-sessCh:
	case <-time.After(2 * time.Second):
		t.Fatal("CSMS never registered the session")
	}
	if _, ok := csms.SessionFor("CP-A5191"); !ok {
		t.Fatal("SessionFor should find the connected charger")
	}

	// StatusNotification + MeterValues update the tapped snapshot.
	cp.call(t, "StatusNotification", map[string]any{"connectorId": 1, "status": "Charging", "errorCode": "NoError"})
	cp.call(t, "MeterValues", map[string]any{
		"connectorId": 1,
		"meterValue": []any{map[string]any{
			"timestamp":    time.Now().UTC().Format(time.RFC3339),
			"sampledValue": []any{map[string]any{"value": "1500", "measurand": "Energy.Active.Import.Register", "unit": "Wh"}},
		}},
	})
	snap := sess.Snapshot()
	if snap.Vendor != "Anker" || snap.Firmware != "1.2.3" {
		t.Errorf("boot fields not tapped: %+v", snap)
	}
	if snap.ConnectorStatus != "Charging" {
		t.Errorf("connector status = %q, want Charging", snap.ConnectorStatus)
	}
	if snap.MeterWh != 1500 {
		t.Errorf("meter = %d Wh, want 1500", snap.MeterWh)
	}

	// StartTransaction: the CSMS assigns a transaction id.
	st := cp.call(t, "StartTransaction", map[string]any{"connectorId": 1, "idTag": "TAG", "meterStart": 0, "timestamp": time.Now().UTC().Format(time.RFC3339)})
	var startResp struct {
		TransactionID int `json:"transactionId"`
	}
	_ = json.Unmarshal(st.Payload, &startResp)
	if startResp.TransactionID == 0 {
		t.Fatal("StartTransaction should return a non-zero transaction id")
	}

	// Now drive every control command and confirm the charger sees it + Accepts.
	cases := []struct {
		name   string
		run    func() (string, error)
		action string
	}{
		{"RemoteStart", func() (string, error) { return sess.RemoteStartTransaction(ctx, "TAG", 1) }, "RemoteStartTransaction"},
		{"SetCurrentLimit", func() (string, error) { return sess.SetCurrentLimit(ctx, 1, 16) }, "SetChargingProfile"},
		{"ClearChargingProfile", func() (string, error) { return sess.ClearChargingProfile(ctx, 1) }, "ClearChargingProfile"},
		{"ChangeAvailability", func() (string, error) { return sess.ChangeAvailability(ctx, 0, false) }, "ChangeAvailability"},
		{"UnlockConnector", func() (string, error) { return sess.UnlockConnector(ctx, 1) }, "UnlockConnector"},
		{"TriggerMessage", func() (string, error) { return sess.TriggerMessage(ctx, "StatusNotification", 1) }, "TriggerMessage"},
		{"ChangeConfiguration", func() (string, error) { return sess.ChangeConfiguration(ctx, "HeartbeatInterval", "60") }, "ChangeConfiguration"},
		{"Reset", func() (string, error) { return sess.Reset(ctx, false) }, "Reset"},
		{"RemoteStop", func() (string, error) { return sess.RemoteStopTransaction(ctx, startResp.TransactionID) }, "RemoteStopTransaction"},
	}
	for _, c := range cases {
		status, err := c.run()
		if err != nil {
			t.Fatalf("%s: %v", c.name, err)
		}
		if status != "Accepted" {
			t.Errorf("%s: status = %q, want Accepted", c.name, status)
		}
		got := cp.waitRecv(t)
		if got.Action != c.action {
			t.Errorf("%s: charger received %q, want %q", c.name, got.Action, c.action)
		}
	}

	// GetConfiguration returns the charger's keys.
	conf, err := sess.GetConfiguration(ctx, nil)
	if err != nil {
		t.Fatalf("GetConfiguration: %v", err)
	}
	cp.waitRecv(t)
	if len(conf.ConfigurationKey) == 0 || conf.ConfigurationKey[0].Key != "HeartbeatInterval" {
		t.Errorf("GetConfiguration = %+v", conf)
	}
}

func TestSessionForUnknownCharger(t *testing.T) {
	csms := NewCSMS(nil)
	if _, ok := csms.SessionFor("nope"); ok {
		t.Fatal("SessionFor should be false for an unconnected charger")
	}
}

func assertStatus(t *testing.T, m Message, want string) {
	t.Helper()
	var r StatusResponse
	if err := json.Unmarshal(m.Payload, &r); err != nil {
		t.Fatalf("decode status: %v", err)
	}
	if r.Status != want {
		t.Fatalf("status = %q, want %q", r.Status, want)
	}
}
