package ocpp

import (
	"context"
	"net/http"
	"strings"
	"sync"
	"testing"
	"time"
)

// wsURL turns an httptest http:// base URL into a ws:// URL with a path.
func wsURL(base, path string) string {
	return "ws" + strings.TrimPrefix(base, "http") + path
}

// simCP is a simulated OCPP 1.6J charge point used by the CSMS and proxy tests.
// It auto-answers inbound control CALLs with "Accepted" (recording them for
// assertions) and can itself send CALLs (BootNotification, StatusNotification, …)
// and await their result.
type simCP struct {
	conn *Conn

	mu      sync.Mutex
	pending map[string]chan Message

	recv chan Message // inbound CALLs (CSMS → CP), for assertions
}

func dialSimCP(t *testing.T, url string, header http.Header) *simCP {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	conn, err := Dial(ctx, url, []string{"ocpp1.6"}, header)
	if err != nil {
		t.Fatalf("dial sim charge point: %v", err)
	}
	cp := &simCP{conn: conn, pending: map[string]chan Message{}, recv: make(chan Message, 64)}
	go cp.loop()
	return cp
}

func (cp *simCP) loop() {
	for {
		data, err := cp.conn.ReadMessage()
		if err != nil {
			return
		}
		msg, err := DecodeMessage(data)
		if err != nil {
			continue
		}
		switch msg.Type {
		case MessageTypeCall:
			select {
			case cp.recv <- msg:
			default:
			}
			out, _ := EncodeCallResult(msg.ID, cp.responseFor(msg.Action))
			_ = cp.conn.WriteMessage(out)
		case MessageTypeCallResult, MessageTypeCallError:
			cp.mu.Lock()
			ch := cp.pending[msg.ID]
			delete(cp.pending, msg.ID)
			cp.mu.Unlock()
			if ch != nil {
				ch <- msg
			}
		}
	}
}

// responseFor returns the payload the sim answers an inbound control CALL with.
func (cp *simCP) responseFor(action string) any {
	switch action {
	case "GetConfiguration":
		return map[string]any{
			"configurationKey": []any{
				map[string]any{"key": "HeartbeatInterval", "readonly": false, "value": "300"},
			},
		}
	default:
		// RemoteStart/Stop, Reset, ChangeAvailability, UnlockConnector,
		// TriggerMessage, SetChargingProfile, ClearChargingProfile, ChangeConfiguration.
		return map[string]any{"status": "Accepted"}
	}
}

// call sends a CALL from the charge point and waits for the reply.
func (cp *simCP) call(t *testing.T, action string, payload any) Message {
	t.Helper()
	id := newMessageID()
	frame, err := EncodeCall(id, action, payload)
	if err != nil {
		t.Fatalf("encode %s: %v", action, err)
	}
	ch := make(chan Message, 1)
	cp.mu.Lock()
	cp.pending[id] = ch
	cp.mu.Unlock()
	if err := cp.conn.WriteMessage(frame); err != nil {
		t.Fatalf("write %s: %v", action, err)
	}
	select {
	case m := <-ch:
		return m
	case <-time.After(5 * time.Second):
		t.Fatalf("timeout awaiting %s reply", action)
		return Message{}
	}
}

// waitRecv returns the next inbound control CALL the sim received, or fails.
func (cp *simCP) waitRecv(t *testing.T) Message {
	t.Helper()
	select {
	case m := <-cp.recv:
		return m
	case <-time.After(5 * time.Second):
		t.Fatal("timeout awaiting inbound control call")
		return Message{}
	}
}

func (cp *simCP) close() { _ = cp.conn.Close() }
