package workspace

import (
	"context"
	"fmt"
	"regexp"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

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

	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	store := New(coredb.WrapSQLDB(sqlDB), Options{
		RequireWorkspaceAccess: func(_ context.Context, userID string, workspaceID string) (access.HumanWorkspaceMember, error) {
			require.Equal(t, "wsp_1", workspaceID)
			return access.HumanWorkspaceMember{Member: model.Member{ID: "mem_" + userID}}, nil
		},
	})
	query := regexp.QuoteMeta(`SELECT state_json, revision, updated_at
FROM channel_sidebar_preferences
WHERE workspace_id = ? AND member_id = ?`)
	updatedAt := time.Date(2026, 8, 7, 1, 2, 3, 0, time.UTC)
	mock.ExpectQuery(query).
		WithArgs("wsp_1", "mem_usr_alice").
		WillReturnRows(sqlmock.NewRows([]string{"state_json", "revision", "updated_at"}).AddRow(`{"Sections":[],"Collapsed":{}}`, 3, updatedAt))
	mock.ExpectQuery(query).
		WithArgs("wsp_1", "mem_usr_bob").
		WillReturnRows(sqlmock.NewRows([]string{"state_json", "revision", "updated_at"}))

	alice, exists, err := store.GetChannelSidebarPreferenceForUser(context.Background(), "usr_alice", "wsp_1")
	require.NoError(t, err)
	require.True(t, exists)
	require.EqualValues(t, 3, alice.Revision)
	require.Equal(t, updatedAt, alice.UpdatedAt)

	_, exists, err = store.GetChannelSidebarPreferenceForUser(context.Background(), "usr_bob", "wsp_1")
	require.NoError(t, err)
	require.False(t, exists)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	store := New(coredb.WrapSQLDB(sqlDB), Options{
		RequireWorkspaceAccess: func(context.Context, string, string) (access.HumanWorkspaceMember, error) {
			return access.HumanWorkspaceMember{Member: model.Member{ID: "mem_1"}}, nil
		},
	})
	storedJSON := `{"Sections":[{"ID":"section_1","Name":"Design","Emoji":"🎨","ChannelIDs":["chn_1","chn_2"]}],"Collapsed":{"section_1":true}}`
	mock.ExpectExec(regexp.QuoteMeta("INSERT INTO channel_sidebar_preferences")).
		WithArgs("wsp_1", "mem_1", storedJSON).
		WillReturnResult(sqlmock.NewResult(1, 1))
	updatedAt := time.Date(2026, 8, 7, 2, 3, 4, 0, time.UTC)
	mock.ExpectQuery(regexp.QuoteMeta("SELECT state_json, revision, updated_at")).
		WithArgs("wsp_1", "mem_1").
		WillReturnRows(sqlmock.NewRows([]string{"state_json", "revision", "updated_at"}).AddRow(storedJSON, 1, updatedAt))

	preference, err := store.PutChannelSidebarPreferenceForUser(context.Background(), "usr_1", "wsp_1", ChannelSidebarPreferenceState{
		Sections: []ChannelSidebarSection{{
			ID:         " section_1 ",
			Name:       " Design ",
			Emoji:      " 🎨 ",
			ChannelIDs: []string{" chn_1 ", "chn_2"},
		}},
		Collapsed: map[string]bool{"section_1": true, "removed": true, "false": false},
	}, nil)

	require.NoError(t, err)
	require.Equal(t, "Design", preference.State.Sections[0].Name)
	require.Equal(t, map[string]bool{"section_1": true}, preference.State.Collapsed)
	require.EqualValues(t, 1, preference.Revision)
	require.Equal(t, updatedAt, preference.UpdatedAt)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	store := New(coredb.WrapSQLDB(sqlDB), Options{
		RequireWorkspaceAccess: func(context.Context, string, string) (access.HumanWorkspaceMember, error) {
			return access.HumanWorkspaceMember{Member: model.Member{ID: "mem_1"}}, nil
		},
	})
	expectedRevision := int64(3)
	mock.ExpectExec(regexp.QuoteMeta("UPDATE channel_sidebar_preferences")).
		WithArgs(`{"Sections":[],"Collapsed":{}}`, "wsp_1", "mem_1", expectedRevision).
		WillReturnResult(sqlmock.NewResult(0, 0))

	_, err = store.PutChannelSidebarPreferenceForUser(
		context.Background(),
		"usr_1",
		"wsp_1",
		ChannelSidebarPreferenceState{Sections: []ChannelSidebarSection{}, Collapsed: map[string]bool{}},
		&expectedRevision,
	)

	require.ErrorIs(t, err, storeerr.ErrConflict)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

	sqlDB, mock, err := sqlmock.New()
	require.NoError(t, err)
	defer sqlDB.Close()

	store := New(coredb.WrapSQLDB(sqlDB), Options{
		RequireWorkspaceAccess: func(context.Context, string, string) (access.HumanWorkspaceMember, error) {
			return access.HumanWorkspaceMember{Member: model.Member{ID: "mem_1"}}, nil
		},
	})
	const storedJSON = `{"Sections":[],"Collapsed":{}}`
	expectedRevision := int64(3)
	mock.ExpectExec(regexp.QuoteMeta("UPDATE channel_sidebar_preferences")).
		WithArgs(storedJSON, "wsp_1", "mem_1", expectedRevision).
		WillReturnResult(sqlmock.NewResult(0, 1))
	updatedAt := time.Date(2026, 8, 7, 2, 3, 4, 0, time.UTC)
	mock.ExpectQuery(regexp.QuoteMeta("SELECT state_json, revision, updated_at")).
		WithArgs("wsp_1", "mem_1").
		WillReturnRows(sqlmock.NewRows([]string{"state_json", "revision", "updated_at"}).
			AddRow(storedJSON, 4, updatedAt))

	preference, err := store.PutChannelSidebarPreferenceForUser(
		context.Background(),
		"usr_1",
		"wsp_1",
		ChannelSidebarPreferenceState{Sections: []ChannelSidebarSection{}, Collapsed: map[string]bool{}},
		&expectedRevision,
	)

	require.NoError(t, err)
	require.EqualValues(t, 4, preference.Revision)
	require.Equal(t, updatedAt, preference.UpdatedAt)
	require.NoError(t, mock.ExpectationsWereMet())
}

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

	_, err := normalizeChannelSidebarPreferenceState(ChannelSidebarPreferenceState{
		Sections: []ChannelSidebarSection{
			{ID: "section_1", Name: "One", Emoji: "1", ChannelIDs: []string{"chn_1"}},
			{ID: "section_2", Name: "Two", Emoji: "2", ChannelIDs: []string{"chn_1"}},
		},
	})
	require.ErrorContains(t, err, "only one sidebar section")
}

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

	for _, sectionID := range []string{channelSidebarPinnedSectionKey, channelSidebarDefaultSectionKey} {
		_, err := normalizeChannelSidebarPreferenceState(ChannelSidebarPreferenceState{
			Sections: []ChannelSidebarSection{{
				ID: sectionID, Name: "Reserved", Emoji: "x", ChannelIDs: []string{},
			}},
		})
		require.ErrorContains(t, err, "reserved")
	}
}

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

	channelIDs := make([]string, maxChannelSidebarChannelIDs+1)
	for index := range channelIDs {
		channelIDs[index] = fmt.Sprintf("chn_%d", index)
	}
	_, err := normalizeChannelSidebarPreferenceState(ChannelSidebarPreferenceState{
		Sections: []ChannelSidebarSection{{
			ID: "section_1", Name: "One", Emoji: "1", ChannelIDs: channelIDs,
		}},
	})
	require.ErrorContains(t, err, "at most 1000 channel assignments")

	collapsed := make(map[string]bool, maxChannelSidebarSections+3)
	for index := 0; index < maxChannelSidebarSections+3; index++ {
		collapsed[fmt.Sprintf("section_%d", index)] = true
	}
	_, err = normalizeChannelSidebarPreferenceState(ChannelSidebarPreferenceState{
		Sections:  []ChannelSidebarSection{},
		Collapsed: collapsed,
	})
	require.ErrorContains(t, err, "at most 52 collapse entries")
}
