package mspmembersync

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

	"github.com/tidbcloud/aisupport/services/jsm-syncer/models"
)

type fakeJSMClient struct {
	usersByOrganization map[string]map[string]struct{}
	listErrors          map[string]error
	addErrors           map[string]error
	listed              []string
	added               []addUsersCall
}

type addUsersCall struct {
	organizationID string
	accountIDs     []string
}

func (f *fakeJSMClient) ListOrganizationUserAccountIDs(_ context.Context, organizationID string) (map[string]struct{}, error) {
	f.listed = append(f.listed, organizationID)
	return f.usersByOrganization[organizationID], f.listErrors[organizationID]
}

func (f *fakeJSMClient) AddUsersToOrganization(_ context.Context, organizationID string, accountIDs []string) error {
	f.added = append(f.added, addUsersCall{organizationID: organizationID, accountIDs: accountIDs})
	return f.addErrors[organizationID]
}

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

	db := newTestDB(t)
	seedOrganizationsAndMembers(t, db)
	jsm := &fakeJSMClient{usersByOrganization: map[string]map[string]struct{}{
		"jsm-1": {"account-1": {}},
	}}
	var output bytes.Buffer

	summary, err := Run(context.Background(), db, jsm, Options{Output: &output})
	require.NoError(t, err)
	require.Equal(t, Summary{
		CustomerOrgsScanned:       2,
		CustomerOrgsSucceeded:     2,
		CustomerOrgsMissingJSM:    1,
		MSPMembersScanned:         3,
		MSPMembersMissingAccount:  1,
		MSPMembersAlreadyPresent:  1,
		MSPMemberAdditionsPlanned: 1,
	}, summary)
	require.Equal(t, []string{"jsm-1"}, jsm.listed)
	require.Empty(t, jsm.added)
	require.Contains(t, output.String(), "skip customer_org=customer-2")
	require.Contains(t, output.String(), "active_msp_members=3 missing_account_id=1 already_present=1 add=1")
}

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

	db := newTestDB(t)
	seedOrganizationsAndMembers(t, db)
	jsm := &fakeJSMClient{usersByOrganization: map[string]map[string]struct{}{
		"jsm-1": {"account-1": {}},
	}}

	summary, err := Run(context.Background(), db, jsm, Options{Execute: true})
	require.NoError(t, err)
	require.Equal(t, 1, summary.MSPMembersAdded)
	require.Equal(t, []addUsersCall{{organizationID: "jsm-1", accountIDs: []string{"account-2"}}}, jsm.added)
}

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

	db := newTestDB(t)
	seedOrganizationsAndMembers(t, db)
	jsm := &fakeJSMClient{usersByOrganization: map[string]map[string]struct{}{
		"jsm-1": {},
	}}

	summary, err := Run(context.Background(), db, jsm, Options{CustomerOrgID: " customer-1 "})
	require.NoError(t, err)
	require.Equal(t, 1, summary.CustomerOrgsScanned)
	require.Equal(t, []string{"jsm-1"}, jsm.listed)
}

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

	db := newTestDB(t)
	mspOrgID := "msp-1"
	jsm1 := "jsm-1"
	jsm2 := "jsm-2"
	require.NoError(t, db.Create([]models.TiDBCloudOrg{
		{ID: "customer-1", Name: "Customer 1", MSPOrgID: &mspOrgID, JSMOrgID: &jsm1},
		{ID: "customer-2", Name: "Customer 2", MSPOrgID: &mspOrgID, JSMOrgID: &jsm2},
	}).Error)
	jsm := &fakeJSMClient{
		usersByOrganization: map[string]map[string]struct{}{"jsm-2": {}},
		listErrors:          map[string]error{"jsm-1": errors.New("unavailable")},
	}

	summary, err := Run(context.Background(), db, jsm, Options{})
	require.EqualError(t, err, "customer organization customer-1: list JSM organization jsm-1 users: unavailable")
	require.Equal(t, 1, summary.CustomerOrgsFailed)
	require.Equal(t, 1, summary.CustomerOrgsSucceeded)
	require.Equal(t, []string{"jsm-1", "jsm-2"}, jsm.listed)
}

func seedOrganizationsAndMembers(t *testing.T, db *gorm.DB) {
	t.Helper()
	mspOrgID := "msp-1"
	jsmOrgID := "jsm-1"
	require.NoError(t, db.Create([]models.TiDBCloudOrg{
		{ID: "msp-1", Name: "MSP"},
		{ID: "customer-1", Name: "Customer 1", MSPOrgID: &mspOrgID, JSMOrgID: &jsmOrgID},
		{ID: "customer-2", Name: "Customer 2", MSPOrgID: &mspOrgID},
		{ID: "not-managed", Name: "Not managed"},
	}).Error)
	account1 := "account-1"
	account2 := "account-2"
	require.NoError(t, db.Create([]models.TiDBCloudUser{
		{ID: "user-1", Email: "one@example.com", AtlassianAccountID: &account1},
		{ID: "user-2", Email: "two@example.com", AtlassianAccountID: &account2},
		{ID: "user-3", Email: "three@example.com"},
		{ID: "user-4", Email: "four@example.com", AtlassianAccountID: &account2},
	}).Error)
	require.NoError(t, db.Create([]models.TiDBCloudOrgMembership{
		{OrgID: "msp-1", UserID: "user-1", Status: models.RelationStatusActive},
		{OrgID: "msp-1", UserID: "user-2", Status: models.RelationStatusActive},
		{OrgID: "msp-1", UserID: "user-3", Status: models.RelationStatusActive},
		{OrgID: "msp-1", UserID: "user-4", Status: models.RelationStatusRemoved},
	}).Error)
}

func newTestDB(t *testing.T) *gorm.DB {
	t.Helper()
	dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&models.TiDBCloudOrg{},
		&models.TiDBCloudUser{},
		&models.TiDBCloudOrgMembership{},
	))
	return db
}
