package userapi

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	userv1 "hyapp.local/api/proto/user/v1"
	"hyapp/pkg/appcode"
	"hyapp/services/gateway-service/internal/auth"
)

type fakeInviteReferrerClient struct {
	attribution *userv1.InviteAttribution
	lastGet     *userv1.GetInviteAttributionRequest
	lastSearch  *userv1.SearchInviteReferrerRequest
	lastBind    *userv1.BindInviteReferrerRequest
}

func (f *fakeInviteReferrerClient) GetInviteAttribution(_ context.Context, req *userv1.GetInviteAttributionRequest) (*userv1.GetInviteAttributionResponse, error) {
	f.lastGet = req
	if f.attribution == nil {
		return &userv1.GetInviteAttributionResponse{Attribution: &userv1.InviteAttribution{}}, nil
	}
	return &userv1.GetInviteAttributionResponse{Attribution: f.attribution}, nil
}

func (f *fakeInviteReferrerClient) SearchInviteReferrer(_ context.Context, req *userv1.SearchInviteReferrerRequest) (*userv1.SearchInviteReferrerResponse, error) {
	f.lastSearch = req
	return &userv1.SearchInviteReferrerResponse{
		Inviter: &userv1.User{
			UserId:        9001,
			DisplayUserId: "163900",
			Username:      "Alice",
			Status:        userv1.UserStatus_USER_STATUS_ACTIVE,
		},
	}, nil
}

func (f *fakeInviteReferrerClient) BindInviteReferrer(_ context.Context, req *userv1.BindInviteReferrerRequest) (*userv1.BindInviteReferrerResponse, error) {
	f.lastBind = req
	return &userv1.BindInviteReferrerResponse{
		Invite: &userv1.InviteBinding{
			Bound:         true,
			InviteCode:    req.GetInviteCode(),
			InviterUserId: 9001,
		},
		Inviter: &userv1.User{
			UserId:        9001,
			DisplayUserId: "163900",
			Username:      "Alice",
			Status:        userv1.UserStatus_USER_STATUS_ACTIVE,
		},
	}, nil
}

func TestInviteReferrerGetReturnsBoundInviter(t *testing.T) {
	client := &fakeInviteReferrerClient{
		attribution: &userv1.InviteAttribution{
			Found:         true,
			InvitedUserId: 10001,
			InviterUserId: 9001,
			InviteCode:    "A1B2C3",
		},
	}
	handler := New(Config{UserInviteClient: client})
	request := newInviteReferrerRequest(http.MethodGet, "/api/v1/users/me/invite-referrer", "")
	recorder := httptest.NewRecorder()

	handler.getMyInviteReferrer(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("get status = %d body=%s", recorder.Code, recorder.Body.String())
	}
	if client.lastGet == nil || client.lastGet.GetInvitedUserId() != 10001 {
		t.Fatalf("get request mismatch: %+v", client.lastGet)
	}
	if client.lastSearch == nil || client.lastSearch.GetUserId() != 10001 || client.lastSearch.GetInviteCode() != "A1B2C3" {
		t.Fatalf("search request mismatch: %+v", client.lastSearch)
	}
	if !strings.Contains(recorder.Body.String(), `"invite_code":"A1B2C3"`) || !strings.Contains(recorder.Body.String(), `"username":"Alice"`) {
		t.Fatalf("bound inviter response mismatch: %s", recorder.Body.String())
	}
}

func TestInviteReferrerGetReturnsEmptyWhenUnbound(t *testing.T) {
	client := &fakeInviteReferrerClient{}
	handler := New(Config{UserInviteClient: client})
	request := newInviteReferrerRequest(http.MethodGet, "/api/v1/users/me/invite-referrer", "")
	recorder := httptest.NewRecorder()

	handler.getMyInviteReferrer(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("get status = %d body=%s", recorder.Code, recorder.Body.String())
	}
	if client.lastGet == nil || client.lastGet.GetInvitedUserId() != 10001 {
		t.Fatalf("get request mismatch: %+v", client.lastGet)
	}
	if client.lastSearch != nil {
		t.Fatalf("unbound relation must not search inviter: %+v", client.lastSearch)
	}
	if strings.Contains(recorder.Body.String(), `"inviter"`) || strings.Contains(recorder.Body.String(), `"invite"`) {
		t.Fatalf("unbound response should not include inviter data: %s", recorder.Body.String())
	}
}

func TestInviteReferrerSearchUsesInviteCode(t *testing.T) {
	client := &fakeInviteReferrerClient{}
	handler := New(Config{UserInviteClient: client})
	request := newInviteReferrerRequest(http.MethodGet, "/api/v1/users/me/invite-referrer/search?invite_code=A1B2C3", "")
	recorder := httptest.NewRecorder()

	handler.searchInviteReferrer(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("search status = %d body=%s", recorder.Code, recorder.Body.String())
	}
	if client.lastSearch == nil || client.lastSearch.GetUserId() != 10001 || client.lastSearch.GetInviteCode() != "A1B2C3" {
		t.Fatalf("search request mismatch: %+v", client.lastSearch)
	}
}

func TestInviteReferrerBindUsesInviteCode(t *testing.T) {
	client := &fakeInviteReferrerClient{}
	handler := New(Config{UserInviteClient: client})
	request := newInviteReferrerRequest(http.MethodPost, "/api/v1/users/me/invite-referrer/bind", `{"invite_code":"A1B2C3"}`)
	recorder := httptest.NewRecorder()

	handler.bindInviteReferrer(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("bind status = %d body=%s", recorder.Code, recorder.Body.String())
	}
	if client.lastBind == nil || client.lastBind.GetUserId() != 10001 || client.lastBind.GetInviteCode() != "A1B2C3" {
		t.Fatalf("bind request mismatch: %+v", client.lastBind)
	}
}

func TestInviteReferrerBindRejectsInviterUserID(t *testing.T) {
	client := &fakeInviteReferrerClient{}
	handler := New(Config{UserInviteClient: client})
	request := newInviteReferrerRequest(http.MethodPost, "/api/v1/users/me/invite-referrer/bind", `{"inviter_user_id":"9001"}`)
	recorder := httptest.NewRecorder()

	handler.bindInviteReferrer(recorder, request)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("bind status = %d body=%s", recorder.Code, recorder.Body.String())
	}
	if client.lastBind != nil {
		t.Fatalf("inviter_user_id body must not be forwarded: %+v", client.lastBind)
	}
}

func newInviteReferrerRequest(method string, target string, body string) *http.Request {
	var reader *strings.Reader
	if body == "" {
		reader = strings.NewReader("")
	} else {
		reader = strings.NewReader(body)
	}
	request := httptest.NewRequest(method, target, reader)
	request.Header.Set("Content-Type", "application/json")
	ctx := appcode.WithContext(request.Context(), "lalu")
	ctx = auth.WithUserID(ctx, 10001)
	return request.WithContext(ctx)
}
