package channel

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

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	storechannel "github.com/tidbcloud/AgentTeam/internal/store/channel"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

type fakeChannelPermissionsStore struct {
	listedWorkspaceID string
	listedChannelID   string
	listResult        []storechannel.ChannelPermission
	listErr           error

	inserted struct {
		workspaceID string
		channelID   string
		memberID    string
		userID      string
		displayName string
		createdBy   string
	}
	insertResult storechannel.ChannelPermission
	insertErr    error

	deleted struct {
		workspaceID string
		channelID   string
		memberID    string
	}
	deleteErr error

	holderChecked struct {
		ctx         context.Context
		userID      string
		workspaceID string
		channelID   string
	}
	holderResult bool
	holderErr    error
}

func (f *fakeChannelPermissionsStore) ListChannelPermissions(_ context.Context, workspaceID string, channelID string) ([]storechannel.ChannelPermission, error) {
	f.listedWorkspaceID = workspaceID
	f.listedChannelID = channelID
	return f.listResult, f.listErr
}

func (f *fakeChannelPermissionsStore) InsertChannelPermission(_ context.Context, workspaceID string, channelID string, memberID string, userID string, displayName string, createdBy string) (storechannel.ChannelPermission, error) {
	f.inserted.workspaceID = workspaceID
	f.inserted.channelID = channelID
	f.inserted.memberID = memberID
	f.inserted.userID = userID
	f.inserted.displayName = displayName
	f.inserted.createdBy = createdBy
	return f.insertResult, f.insertErr
}

func (f *fakeChannelPermissionsStore) DeleteChannelPermission(_ context.Context, workspaceID string, channelID string, memberID string) error {
	f.deleted.workspaceID = workspaceID
	f.deleted.channelID = channelID
	f.deleted.memberID = memberID
	return f.deleteErr
}

func (f *fakeChannelPermissionsStore) IsChannelPermissionHolderForUser(ctx context.Context, userID string, workspaceID string, channelID string) (bool, error) {
	f.holderChecked.ctx = ctx
	f.holderChecked.userID = userID
	f.holderChecked.workspaceID = workspaceID
	f.holderChecked.channelID = channelID
	return f.holderResult, f.holderErr
}

type fakeChannelAccessStore struct {
	resolvedCtx         context.Context
	resolvedUserID      string
	resolvedWorkspaceID string
	resolvedChannelID   string
	resolution          storechannel.ChannelAccessResolution
	err                 error
}

func (f *fakeChannelAccessStore) ResolveChannelAccessForUser(ctx context.Context, userID string, workspaceID string, channelID string) (storechannel.ChannelAccessResolution, error) {
	f.resolvedCtx = ctx
	f.resolvedUserID = userID
	f.resolvedWorkspaceID = workspaceID
	f.resolvedChannelID = channelID
	return f.resolution, f.err
}

func newPermissionsHandler(store *fakeChannelPermissionsStore, access *fakeChannelAccessStore) *ChannelPermissionsHandler {
	return &ChannelPermissionsHandler{store: store, channels: access}
}

func joinedResolution(canManage bool) storechannel.ChannelAccessResolution {
	return storechannel.ChannelAccessResolution{
		Status:  storechannel.ChannelAccessStatusJoined,
		Channel: message.ChannelSummary{ChannelID: "chn_1", CanManage: canManage},
	}
}

func TestChannelPermissionsHandlerListDelegatesToStore(t *testing.T) {
	t.Parallel()

	store := &fakeChannelPermissionsStore{listResult: []storechannel.ChannelPermission{{
		ID:          "chperm_1",
		WorkspaceID: "wsp_1",
		ChannelID:   "chn_1",
		MemberID:    "mem_1",
		UserID:      "usr_2",
		DisplayName: "Alice",
		CreatedBy:   "usr_1",
	}}}
	access := &fakeChannelAccessStore{resolution: joinedResolution(false)}
	handler := newPermissionsHandler(store, access)

	response, err := handler.List(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "wsp_1", store.listedWorkspaceID)
	require.Equal(t, "chn_1", store.listedChannelID)
	require.Equal(t, []dto.ChannelPermissionResponse{{
		ID:          "chperm_1",
		WorkspaceID: "wsp_1",
		ChannelID:   "chn_1",
		MemberID:    "mem_1",
		UserID:      "usr_2",
		DisplayName: "Alice",
		CreatedBy:   "usr_1",
	}}, response.Body)
}

func TestChannelPermissionsHandlerListRequiresChannelID(t *testing.T) {
	t.Parallel()

	handler := newPermissionsHandler(&fakeChannelPermissionsStore{}, &fakeChannelAccessStore{})
	_, err := handler.List(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: " "}))
	requireHTTPError(t, err, http.StatusBadRequest, "channelId is required")
}

func TestChannelPermissionsHandlerListForbiddenWhenNotJoined(t *testing.T) {
	t.Parallel()

	store := &fakeChannelPermissionsStore{}
	access := &fakeChannelAccessStore{resolution: storechannel.ChannelAccessResolution{Status: storechannel.ChannelAccessStatusInaccessible}}
	handler := newPermissionsHandler(store, access)

	_, err := handler.List(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusForbidden, "forbidden")
	require.Empty(t, store.listedChannelID)
}

func TestChannelPermissionsHandlerGrantInsertsAndMapsDuplicateConflict(t *testing.T) {
	t.Parallel()

	store := &fakeChannelPermissionsStore{
		insertResult: storechannel.ChannelPermission{ID: "chperm_new", WorkspaceID: "wsp_1", ChannelID: "chn_1", MemberID: "mem_2"},
	}
	access := &fakeChannelAccessStore{resolution: joinedResolution(true)}
	handler := newPermissionsHandler(store, access)

	body := dto.ChannelPermissionRequest{MemberID: "mem_2", UserID: "usr_2", DisplayName: "Alice"}
	response, err := handler.Grant(channelWorkspaceJSONWithParams(body, gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, dto.EmptyResponse{}, response.Body)
	require.Equal(t, "chn_1", store.inserted.channelID)
	require.Equal(t, "mem_2", store.inserted.memberID)
	require.Equal(t, "usr_2", store.inserted.userID)
	require.Equal(t, "Alice", store.inserted.displayName)
	require.Equal(t, "usr_1", store.inserted.createdBy)

	store.insertErr = storechannel.ErrChannelPermissionAlreadyGranted
	_, err = handler.Grant(channelWorkspaceJSONWithParams(body, gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusConflict, "member already holds the channel permission")
}

func TestChannelPermissionsHandlerGrantValidatesRequiredFields(t *testing.T) {
	t.Parallel()

	handler := newPermissionsHandler(&fakeChannelPermissionsStore{}, &fakeChannelAccessStore{resolution: joinedResolution(true)})

	_, err := handler.Grant(channelWorkspaceJSONWithParams(dto.ChannelPermissionRequest{}, gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusBadRequest, "memberId is required")

	_, err = handler.Grant(channelWorkspaceJSONWithParams(dto.ChannelPermissionRequest{MemberID: "mem_2"}, gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusBadRequest, "userId is required")

	_, err = handler.Grant(channelWorkspaceJSONWithParams(dto.ChannelPermissionRequest{MemberID: "mem_2", UserID: "usr_2"}, gin.Param{Key: "channelId", Value: " "}))
	requireHTTPError(t, err, http.StatusBadRequest, "channelId is required")
}

func TestChannelPermissionsHandlerGrantForbiddenWhenNotJoined(t *testing.T) {
	t.Parallel()

	store := &fakeChannelPermissionsStore{}
	access := &fakeChannelAccessStore{resolution: storechannel.ChannelAccessResolution{Status: storechannel.ChannelAccessStatusPublicJoinable}}
	handler := newPermissionsHandler(store, access)

	body := dto.ChannelPermissionRequest{MemberID: "mem_2", UserID: "usr_2", DisplayName: "Alice"}
	_, err := handler.Grant(channelWorkspaceJSONWithParams(body, gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusForbidden, "forbidden")
	require.Empty(t, store.inserted.channelID)
	require.Empty(t, store.holderChecked.channelID)
}

func TestChannelPermissionsHandlerGrantForbiddenForNonOwnerNonHolder(t *testing.T) {
	t.Parallel()

	store := &fakeChannelPermissionsStore{holderResult: false}
	access := &fakeChannelAccessStore{resolution: joinedResolution(false)}
	handler := newPermissionsHandler(store, access)

	body := dto.ChannelPermissionRequest{MemberID: "mem_2", UserID: "usr_2", DisplayName: "Alice"}
	_, err := handler.Grant(channelWorkspaceJSONWithParams(body, gin.Param{Key: "channelId", Value: "chn_1"}))
	requireHTTPError(t, err, http.StatusForbidden, "forbidden")
	require.Equal(t, "chn_1", store.holderChecked.channelID)
	require.Empty(t, store.inserted.channelID)
}

func TestChannelPermissionsHandlerGrantSucceedsForPermissionHolder(t *testing.T) {
	t.Parallel()

	store := &fakeChannelPermissionsStore{
		holderResult: true,
		insertResult: storechannel.ChannelPermission{ID: "chperm_new", WorkspaceID: "wsp_1", ChannelID: "chn_1", MemberID: "mem_2"},
	}
	access := &fakeChannelAccessStore{resolution: joinedResolution(false)}
	handler := newPermissionsHandler(store, access)

	body := dto.ChannelPermissionRequest{MemberID: "mem_2", UserID: "usr_2", DisplayName: "Alice"}
	_, err := handler.Grant(channelWorkspaceJSONWithParams(body, gin.Param{Key: "channelId", Value: "chn_1"}))
	require.NoError(t, err)
	require.Equal(t, "chn_1", store.inserted.channelID)
}

func TestChannelPermissionsHandlerRevokeDeletesAndMapsMissing(t *testing.T) {
	t.Parallel()

	store := &fakeChannelPermissionsStore{}
	access := &fakeChannelAccessStore{resolution: joinedResolution(true)}
	handler := newPermissionsHandler(store, access)

	response, err := handler.Revoke(channelWorkspaceRequest(
		gin.Param{Key: "channelId", Value: "chn_1"},
		gin.Param{Key: "memberId", Value: "mem_2"},
	))
	require.NoError(t, err)
	require.Equal(t, dto.EmptyResponse{}, response.Body)
	require.Equal(t, "chn_1", store.deleted.channelID)
	require.Equal(t, "mem_2", store.deleted.memberID)

	store.deleteErr = storechannel.ErrChannelPermissionNotGranted
	_, err = handler.Revoke(channelWorkspaceRequest(
		gin.Param{Key: "channelId", Value: "chn_1"},
		gin.Param{Key: "memberId", Value: "mem_2"},
	))
	requireHTTPError(t, err, http.StatusNotFound, "member does not hold the channel permission")
}

func TestChannelPermissionsHandlerRevokeValidatesParams(t *testing.T) {
	t.Parallel()

	handler := newPermissionsHandler(&fakeChannelPermissionsStore{}, &fakeChannelAccessStore{resolution: joinedResolution(true)})

	_, err := handler.Revoke(channelWorkspaceRequest(gin.Param{Key: "channelId", Value: " "}))
	requireHTTPError(t, err, http.StatusBadRequest, "channelId and memberId are required")
}

func TestChannelPermissionsHandlerRevokeForbiddenForNonOwnerNonHolder(t *testing.T) {
	t.Parallel()

	store := &fakeChannelPermissionsStore{holderResult: false}
	access := &fakeChannelAccessStore{resolution: joinedResolution(false)}
	handler := newPermissionsHandler(store, access)

	_, err := handler.Revoke(channelWorkspaceRequest(
		gin.Param{Key: "channelId", Value: "chn_1"},
		gin.Param{Key: "memberId", Value: "mem_2"},
	))
	requireHTTPError(t, err, http.StatusForbidden, "forbidden")
	require.Empty(t, store.deleted.channelID)
}

func TestChannelPermissionsHandlerRevokeSucceedsForPermissionHolder(t *testing.T) {
	t.Parallel()

	store := &fakeChannelPermissionsStore{holderResult: true}
	access := &fakeChannelAccessStore{resolution: joinedResolution(false)}
	handler := newPermissionsHandler(store, access)

	_, err := handler.Revoke(channelWorkspaceRequest(
		gin.Param{Key: "channelId", Value: "chn_1"},
		gin.Param{Key: "memberId", Value: "mem_2"},
	))
	require.NoError(t, err)
	require.Equal(t, "chn_1", store.deleted.channelID)
}

func TestChannelPermissionsHandlerPropagatesRequestContext(t *testing.T) {
	t.Parallel()

	t.Run("list forwards ctx to access resolution", func(t *testing.T) {
		t.Parallel()

		store := &fakeChannelPermissionsStore{}
		access := &fakeChannelAccessStore{resolution: joinedResolution(false)}
		handler := newPermissionsHandler(store, access)

		req := channelWorkspaceRequest(gin.Param{Key: "channelId", Value: "chn_1"})
		_, err := handler.List(req)
		require.NoError(t, err)
		require.Equal(t, req.Context(), access.resolvedCtx)
	})

	t.Run("grant forwards ctx to access resolution and holder check", func(t *testing.T) {
		t.Parallel()

		store := &fakeChannelPermissionsStore{holderResult: true}
		access := &fakeChannelAccessStore{resolution: joinedResolution(false)}
		handler := newPermissionsHandler(store, access)

		body := dto.ChannelPermissionRequest{MemberID: "mem_2", UserID: "usr_2", DisplayName: "Alice"}
		req := channelWorkspaceJSONWithParams(body, gin.Param{Key: "channelId", Value: "chn_1"})
		_, err := handler.Grant(req)
		require.NoError(t, err)
		require.Equal(t, req.Context(), access.resolvedCtx)
		require.Equal(t, req.Context(), store.holderChecked.ctx)
	})

	t.Run("revoke forwards ctx to access resolution and holder check", func(t *testing.T) {
		t.Parallel()

		store := &fakeChannelPermissionsStore{holderResult: true}
		access := &fakeChannelAccessStore{resolution: joinedResolution(false)}
		handler := newPermissionsHandler(store, access)

		req := channelWorkspaceRequest(
			gin.Param{Key: "channelId", Value: "chn_1"},
			gin.Param{Key: "memberId", Value: "mem_2"},
		)
		_, err := handler.Revoke(req)
		require.NoError(t, err)
		require.Equal(t, req.Context(), access.resolvedCtx)
		require.Equal(t, req.Context(), store.holderChecked.ctx)
	})
}
