package jsmorgdedupe

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

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

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

type fakeMaintenanceClient struct {
	searchResults map[string][]atlassian.Organization
	searchErrors  map[string]error
	searched      []string
	deleted       []string
	renamed       []renameCall
}

type renameCall struct {
	id   string
	name string
}

func (f *fakeMaintenanceClient) FindOrganizationsByTiDBCloudOrgID(_ context.Context, query string) ([]atlassian.Organization, error) {
	f.searched = append(f.searched, query)
	return f.searchResults[query], f.searchErrors[query]
}

func (f *fakeMaintenanceClient) DeleteOrganization(_ context.Context, organizationID string) error {
	f.deleted = append(f.deleted, organizationID)
	return nil
}

func (f *fakeMaintenanceClient) RenameOrganization(_ context.Context, organizationID string, newName string) error {
	f.renamed = append(f.renamed, renameCall{id: organizationID, name: newName})
	return nil
}

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

	db := newTestDB(t)
	targetOldJSMID := "150003"
	boundaryJSMID := "140000"
	basicJSMID := "180000"
	require.NoError(t, db.Create([]models.TiDBCloudOrg{
		{ID: "1372813089209250455", Name: "Acme", Status: "active", SupportPlan: "Premium", JSMOrgID: &targetOldJSMID},
		{ID: "boundary", Name: "Boundary", Status: "active", SupportPlan: "Enterprise", JSMOrgID: &boundaryJSMID},
		{ID: "basic", Name: "Basic", Status: "active", SupportPlan: "Basic", JSMOrgID: &basicJSMID},
	}).Error)

	jsm := &fakeMaintenanceClient{searchResults: map[string][]atlassian.Organization{
		"1372813089209250455": {
			{ID: "22570"},
			{ID: "22568"},
			{ID: "22569"},
			{ID: "22570"},
		},
	}}
	var output bytes.Buffer
	summary, err := Run(context.Background(), db, jsm, Options{Execute: true, Output: &output})
	require.NoError(t, err)
	require.Equal(t, Summary{
		Scanned:        1,
		Succeeded:      1,
		Duplicates:     2,
		Deleted:        2,
		DatabaseWrites: 1,
		Renamed:        1,
	}, summary)
	require.Equal(t, []string{"22569", "22570"}, jsm.deleted)
	require.Equal(t, []renameCall{{id: "22568", name: "Acme (1372813089209250455)"}}, jsm.renamed)

	var updated models.TiDBCloudOrg
	require.NoError(t, db.First(&updated, "id = ?", "1372813089209250455").Error)
	require.NotNil(t, updated.JSMOrgID)
	require.Equal(t, "22568", *updated.JSMOrgID)
	require.Contains(t, output.String(), "keep_jsm_org_id=22568")
}

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

	db := newTestDB(t)
	oldJSMID := "150003"
	require.NoError(t, db.Create(&models.TiDBCloudOrg{
		ID:          "1372813089209250455",
		Name:        "Acme",
		Status:      "active",
		SupportPlan: "Developer",
		JSMOrgID:    &oldJSMID,
	}).Error)

	jsm := &fakeMaintenanceClient{searchResults: map[string][]atlassian.Organization{
		"1372813089209250455": {{ID: "22568"}, {ID: "22569"}},
	}}
	summary, err := Run(context.Background(), db, jsm, Options{})
	require.NoError(t, err)
	require.Equal(t, 1, summary.Scanned)
	require.Equal(t, 1, summary.Succeeded)
	require.Equal(t, 1, summary.Duplicates)
	require.Zero(t, summary.Deleted)
	require.Zero(t, summary.DatabaseWrites)
	require.Zero(t, summary.Renamed)
	require.Empty(t, jsm.deleted)
	require.Empty(t, jsm.renamed)

	var unchanged models.TiDBCloudOrg
	require.NoError(t, db.First(&unchanged, "id = ?", "1372813089209250455").Error)
	require.NotNil(t, unchanged.JSMOrgID)
	require.Equal(t, oldJSMID, *unchanged.JSMOrgID)
}

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

	db := newTestDB(t)
	oldJSMID := "150003"
	require.NoError(t, db.Create(&models.TiDBCloudOrg{
		ID:          "1372813089209250455",
		Name:        "Acme",
		Status:      "active",
		SupportPlan: "Premium",
		JSMOrgID:    &oldJSMID,
	}).Error)

	jsm := &fakeMaintenanceClient{searchResults: map[string][]atlassian.Organization{
		"1372813089209250455": {{ID: oldJSMID}},
	}}
	summary, err := Run(context.Background(), db, jsm, Options{Execute: true})
	require.NoError(t, err)
	require.Equal(t, 1, summary.Succeeded)
	require.Zero(t, summary.DatabaseWrites)
	require.Equal(t, 1, summary.Renamed)
	require.Equal(t, []renameCall{{id: oldJSMID, name: "Acme (1372813089209250455)"}}, jsm.renamed)
}

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

	db := newTestDB(t)
	firstJSMID := "150001"
	secondJSMID := "150002"
	require.NoError(t, db.Create([]models.TiDBCloudOrg{
		{ID: "org-1", Name: "First", Status: "active", SupportPlan: "Premium", JSMOrgID: &firstJSMID},
		{ID: "org-2", Name: "Second", Status: "active", SupportPlan: "Premium", JSMOrgID: &secondJSMID},
	}).Error)

	jsm := &fakeMaintenanceClient{
		searchResults: map[string][]atlassian.Organization{
			"org-2": {{ID: "22568"}},
		},
		searchErrors: map[string]error{"org-1": fmt.Errorf("search unavailable")},
	}
	summary, err := Run(context.Background(), db, jsm, Options{Execute: true})
	require.EqualError(t, err, "organization org-1: search JSM organizations: search unavailable")
	require.Equal(t, 2, summary.Scanned)
	require.Zero(t, summary.Succeeded)
	require.Equal(t, 1, summary.Failed)
	require.Equal(t, []string{"org-1"}, jsm.searched)
	require.Empty(t, jsm.deleted)
	require.Empty(t, jsm.renamed)
}

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

	_, err := sortedUniqueOrganizationIDs([]atlassian.Organization{{ID: "not-a-number"}})
	require.EqualError(t, err, `JSM organization search returned non-numeric ID "not-a-number"`)
}

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{}))
	return db
}
