package mxgateway

import (
	"context"
	"errors"
	"strings"
	"testing"

	pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
)

// TestAdviseSupervisoryBuildsCommandAndExposesRawReply pins that the promoted
// typed helper emits an ADVISE_SUPERVISORY command carrying the server/item
// handles and returns the raw reply.
func TestAdviseSupervisoryBuildsCommandAndExposesRawReply(t *testing.T) {
	fake := &fakeGatewayServer{
		invokeReply: &pb.MxCommandReply{
			SessionId:      "session-1",
			Kind:           pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY,
			ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK},
		},
	}
	client, cleanup := newBufconnClient(t, fake)
	defer cleanup()
	session := NewSessionForID(client, "session-1")

	reply, err := session.AdviseSupervisoryRaw(context.Background(), 12, 34)
	if err != nil {
		t.Fatalf("AdviseSupervisoryRaw() error = %v", err)
	}
	if reply.GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY {
		t.Fatalf("reply kind = %s", reply.GetKind())
	}
	cmd := fake.invokeRequest.GetCommand()
	if cmd.GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_ADVISE_SUPERVISORY {
		t.Fatalf("command kind = %s", cmd.GetKind())
	}
	if cmd.GetAdviseSupervisory().GetServerHandle() != 12 || cmd.GetAdviseSupervisory().GetItemHandle() != 34 {
		t.Fatalf("advise-supervisory handles = (%d, %d), want (12, 34)",
			cmd.GetAdviseSupervisory().GetServerHandle(), cmd.GetAdviseSupervisory().GetItemHandle())
	}

	// The error-returning wrapper drops the reply but must not error on success.
	if err := session.AdviseSupervisory(context.Background(), 12, 34); err != nil {
		t.Fatalf("AdviseSupervisory() error = %v", err)
	}
}

// TestWriteSecuredSurfacesNativeFailureWithoutPriorAuthenticate pins MXAccess
// parity: WriteSecured issued without a preceding AuthenticateUser is rejected
// natively, and the client surfaces that failure as a typed MxAccessError rather
// than pre-validating or reordering. The write value is also kept out of the
// surfaced error text.
func TestWriteSecuredSurfacesNativeFailureWithoutPriorAuthenticate(t *testing.T) {
	hresult := int32(-2147024891) // E_ACCESSDENIED
	fake := &fakeGatewayServer{
		invokeReply: &pb.MxCommandReply{
			SessionId:         "session-1",
			Kind:              pb.MxCommandKind_MX_COMMAND_KIND_WRITE_SECURED,
			Hresult:           &hresult,
			DiagnosticMessage: "WriteSecured requires a prior AuthenticateUser",
			ProtocolStatus: &pb.ProtocolStatus{
				Code:    pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE,
				Message: "MXAccess failed",
			},
		},
	}
	client, cleanup := newBufconnClient(t, fake)
	defer cleanup()
	session := NewSessionForID(client, "session-1")

	securedValue := "supersecret-payload"
	err := session.WriteSecured(context.Background(), 12, 34, 0, 0, StringValue(securedValue))

	var mxErr *MxAccessError
	if !errors.As(err, &mxErr) {
		t.Fatalf("error %T does not support errors.As(*MxAccessError); err = %v", err, err)
	}
	if strings.Contains(err.Error(), securedValue) {
		t.Fatalf("surfaced error leaked the secured payload: %q", err.Error())
	}

	// The command must carry the secured fields verbatim (parity: unaltered).
	cmd := fake.invokeRequest.GetCommand()
	if cmd.GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_WRITE_SECURED {
		t.Fatalf("command kind = %s", cmd.GetKind())
	}
	if cmd.GetWriteSecured().GetValue().GetStringValue() != securedValue {
		t.Fatalf("wire value = %q, want %q", cmd.GetWriteSecured().GetValue().GetStringValue(), securedValue)
	}
}

// TestWriteSecuredRejectsNilValueWithoutRoundTrip pins the client-side required
// guard, which never echoes the (absent) value.
func TestWriteSecuredRejectsNilValue(t *testing.T) {
	fake := &fakeGatewayServer{}
	client, cleanup := newBufconnClient(t, fake)
	defer cleanup()
	session := NewSessionForID(client, "session-1")

	if err := session.WriteSecured(context.Background(), 1, 2, 0, 0, nil); err == nil ||
		!strings.Contains(err.Error(), "write-secured value is required") {
		t.Fatalf("WriteSecured(nil value) error = %v", err)
	}
}

// TestAuthenticateUserReturnsUserIDOnHappyPath pins the typed helper unpacking of
// AuthenticateUserReply.user_id and that the credential is carried on the wire
// but never surfaced.
func TestAuthenticateUserReturnsUserIDOnHappyPath(t *testing.T) {
	fake := &fakeGatewayServer{
		invokeReply: &pb.MxCommandReply{
			SessionId:      "session-1",
			Kind:           pb.MxCommandKind_MX_COMMAND_KIND_AUTHENTICATE_USER,
			ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK},
			Payload: &pb.MxCommandReply_AuthenticateUser{
				AuthenticateUser: &pb.AuthenticateUserReply{UserId: 4242},
			},
		},
	}
	client, cleanup := newBufconnClient(t, fake)
	defer cleanup()
	session := NewSessionForID(client, "session-1")

	userID, err := session.AuthenticateUser(context.Background(), 12, "operator", "hunter2-password")
	if err != nil {
		t.Fatalf("AuthenticateUser() error = %v", err)
	}
	if userID != 4242 {
		t.Fatalf("user id = %d, want 4242", userID)
	}

	cmd := fake.invokeRequest.GetCommand()
	if cmd.GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_AUTHENTICATE_USER {
		t.Fatalf("command kind = %s", cmd.GetKind())
	}
	if cmd.GetAuthenticateUser().GetVerifyUser() != "operator" {
		t.Fatalf("verify user = %q, want operator", cmd.GetAuthenticateUser().GetVerifyUser())
	}
	if cmd.GetAuthenticateUser().GetVerifyUserPassword() != "hunter2-password" {
		t.Fatalf("password not carried to the wire verbatim")
	}
}

// TestAuthenticateUserScrubsCredentialFromSurfacedError proves the redaction
// seam: even when the gateway diagnostic message echoes the credential back, the
// surfaced error redacts it while the typed MxAccessError remains reachable via
// errors.As.
func TestAuthenticateUserScrubsCredentialFromSurfacedError(t *testing.T) {
	password := "hunter2-password"
	fake := &fakeGatewayServer{
		invokeReply: &pb.MxCommandReply{
			SessionId:         "session-1",
			Kind:              pb.MxCommandKind_MX_COMMAND_KIND_AUTHENTICATE_USER,
			DiagnosticMessage: "authentication failed for password " + password,
			// Message is intentionally left empty so MxAccessError.Error() falls
			// through to the diagnostic message — the free-text field that could
			// otherwise echo the credential back to a caller's log.
			ProtocolStatus: &pb.ProtocolStatus{
				Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE,
			},
		},
	}
	client, cleanup := newBufconnClient(t, fake)
	defer cleanup()
	session := NewSessionForID(client, "session-1")

	_, err := session.AuthenticateUser(context.Background(), 12, "operator", password)
	if err == nil {
		t.Fatal("AuthenticateUser() returned no error on native failure")
	}
	if strings.Contains(err.Error(), password) {
		t.Fatalf("surfaced error leaked the credential: %q", err.Error())
	}
	if !strings.Contains(err.Error(), redactedSecretMarker) {
		t.Fatalf("surfaced error missing redaction marker: %q", err.Error())
	}
	var mxErr *MxAccessError
	if !errors.As(err, &mxErr) {
		t.Fatalf("redaction wrapper broke errors.As(*MxAccessError); err = %v", err)
	}
}

// TestAuthenticateUserRequiresVerifyUser pins the client-side required guard.
func TestAuthenticateUserRequiresVerifyUser(t *testing.T) {
	fake := &fakeGatewayServer{}
	client, cleanup := newBufconnClient(t, fake)
	defer cleanup()
	session := NewSessionForID(client, "session-1")

	if _, err := session.AuthenticateUser(context.Background(), 12, "", "pw"); err == nil ||
		!strings.Contains(err.Error(), "verify user is required") {
		t.Fatalf("AuthenticateUser(empty user) error = %v", err)
	}
}

// TestSuspendActivateReturnStatus covers two Phase 2 helpers that unpack an
// MxStatusProxy from their dedicated reply arms.
func TestSuspendActivateReturnStatus(t *testing.T) {
	fake := &fakeGatewayServer{
		invokeReply: &pb.MxCommandReply{
			SessionId:      "session-1",
			Kind:           pb.MxCommandKind_MX_COMMAND_KIND_SUSPEND,
			ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK},
			Payload: &pb.MxCommandReply_Suspend{
				Suspend: &pb.SuspendReply{Status: &pb.MxStatusProxy{Success: 1, DiagnosticText: "suspended"}},
			},
		},
	}
	client, cleanup := newBufconnClient(t, fake)
	defer cleanup()
	session := NewSessionForID(client, "session-1")

	status, err := session.Suspend(context.Background(), 12, 34)
	if err != nil {
		t.Fatalf("Suspend() error = %v", err)
	}
	if status.GetDiagnosticText() != "suspended" {
		t.Fatalf("status diagnostic = %q, want suspended", status.GetDiagnosticText())
	}
	if fake.invokeRequest.GetCommand().GetSuspend().GetItemHandle() != 34 {
		t.Fatalf("suspend item handle = %d, want 34", fake.invokeRequest.GetCommand().GetSuspend().GetItemHandle())
	}
}
