package countryregion

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/integration/roomclient"
	"hyapp-admin-server/internal/model"
	"hyapp-admin-server/internal/repository"
)

func TestRenameCountryCodeRejectsInvalidCode(t *testing.T) {
	db, mock, closeDB := newCountryRenameMockDB(t)
	defer closeDB()
	store := &fakeCountryRenameJobStore{}
	service := NewService(nil, db, store, nil)

	_, err := service.RenameCountryCode(appctx.WithContext(context.Background(), "lalu"), 7, "admin", 12, "req-1", validRenameCountryCodeRequest("P1"))
	if !errors.Is(err, errCountryInvalid) {
		t.Fatalf("expected invalid country error, got %v", err)
	}
	assertCountryRenameExpectations(t, mock)
	if len(store.created) != 0 {
		t.Fatalf("invalid request should not create job, got %d", len(store.created))
	}
}

func TestRenameCountryCodeRejectsDuplicateCode(t *testing.T) {
	db, mock, closeDB := newCountryRenameMockDB(t)
	defer closeDB()
	store := &fakeCountryRenameJobStore{}
	service := NewService(nil, db, store, nil)

	mock.ExpectBegin()
	expectCountryRenameLockedCountry(mock, "PH")
	expectCountryRenameDuplicateCode(mock, "PHL", 88)
	mock.ExpectRollback()

	_, err := service.RenameCountryCode(appctx.WithContext(context.Background(), "lalu"), 7, "admin", 12, "req-1", validRenameCountryCodeRequest("PHL"))
	if !errors.Is(err, errCountryConflict) {
		t.Fatalf("expected duplicate country error, got %v", err)
	}
	assertCountryRenameExpectations(t, mock)
	if len(store.created) != 0 {
		t.Fatalf("duplicate country code should not create job, got %d", len(store.created))
	}
}

func TestRenameCountryCodeRejectsActiveJobConflict(t *testing.T) {
	db, mock, closeDB := newCountryRenameMockDB(t)
	defer closeDB()
	payload := countryCodeRenameJobPayload{
		AppCode:        "lalu",
		CountryID:      12,
		OldCountryCode: "PH",
		NewCountryCode: "PHL",
	}
	body, _ := json.Marshal(payload)
	store := &fakeCountryRenameJobStore{active: []model.AdminJob{{PayloadJSON: string(body)}}}
	service := NewService(nil, db, store, nil)

	mock.ExpectBegin()
	expectCountryRenameLockedCountry(mock, "PH")
	expectCountryRenameAvailableCode(mock, "PHL")
	mock.ExpectRollback()

	_, err := service.RenameCountryCode(appctx.WithContext(context.Background(), "lalu"), 7, "admin", 12, "req-1", validRenameCountryCodeRequest("PHL"))
	if !errors.Is(err, errCountryRenameJobConflict) {
		t.Fatalf("expected active job conflict, got %v", err)
	}
	assertCountryRenameExpectations(t, mock)
	if len(store.created) != 0 {
		t.Fatalf("active conflict should not create job, got %d", len(store.created))
	}
}

func TestRenameCountryCodeCreatesPendingJob(t *testing.T) {
	db, mock, closeDB := newCountryRenameMockDB(t)
	defer closeDB()
	store := &fakeCountryRenameJobStore{}
	service := NewService(nil, db, store, nil)

	mock.ExpectBegin()
	expectCountryRenameLockedCountry(mock, "PH")
	expectCountryRenameAvailableCode(mock, "PHL")
	mock.ExpectCommit()

	resp, err := service.RenameCountryCode(appctx.WithContext(context.Background(), "lalu"), 7, "admin", 12, "req-1", validRenameCountryCodeRequest("phl"))
	if err != nil {
		t.Fatalf("rename country code failed: %v", err)
	}
	assertCountryRenameExpectations(t, mock)
	if resp.JobID != 101 || resp.Status != model.JobStatusPending || resp.OldCountryCode != "PH" || resp.NewCountryCode != "PHL" {
		t.Fatalf("unexpected response: %+v", resp)
	}
	if len(store.created) != 1 {
		t.Fatalf("expected one created job, got %d", len(store.created))
	}
	var payload countryCodeRenameJobPayload
	if err := json.Unmarshal([]byte(store.created[0].PayloadJSON), &payload); err != nil {
		t.Fatalf("decode payload: %v", err)
	}
	if payload.CountryID != 12 || payload.OldCountryCode != "PH" || payload.NewCountryCode != "PHL" || payload.ActorID != 7 {
		t.Fatalf("unexpected payload: %+v", payload)
	}
}

func TestHandleCountryCodeRenameJobUpdatesCurrentStateAndReturnsCounts(t *testing.T) {
	db, mock, closeDB := newCountryRenameMockDB(t)
	defer closeDB()
	store := &fakeCountryRenameJobStore{
		adminCounts: repository.CountryCodeAdminReferenceCounts{Banners: 2, SplashScreens: 3},
	}
	room := &fakeCountryRenameRoomClient{
		result: roomclient.RenameOwnerCountryCodeResult{RoomListEntries: 4, RoomSnapshots: 5},
	}
	service := NewService(nil, db, store, room)

	mock.ExpectBegin()
	expectCountryRenameLockedCountry(mock, "PH")
	expectCountryRenameAvailableCode(mock, "PHL")
	mock.ExpectExec("UPDATE countries").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE region_countries").WillReturnResult(sqlmock.NewResult(0, 6))
	mock.ExpectExec("UPDATE users").WillReturnResult(sqlmock.NewResult(0, 7))
	mock.ExpectExec("UPDATE user_region_rebuild_tasks").WillReturnResult(sqlmock.NewResult(0, 8))
	mock.ExpectCommit()

	payload := countryCodeRenameJobPayload{
		AppCode:            "lalu",
		CountryID:          12,
		OldCountryCode:     "PH",
		NewCountryCode:     "PHL",
		CountryName:        "Philippines",
		CountryDisplayName: "菲律宾",
		ActorID:            7,
		RequestID:          "req-1",
	}
	body, _ := json.Marshal(payload)
	resultJSON, err := service.HandleCountryCodeRenameJob(context.Background(), &model.AdminJob{ID: 101, PayloadJSON: string(body)})
	if err != nil {
		t.Fatalf("handle job failed: %v", err)
	}
	assertCountryRenameExpectations(t, mock)

	var result countryCodeRenameResult
	if err := json.Unmarshal([]byte(resultJSON), &result); err != nil {
		t.Fatalf("decode result: %v", err)
	}
	if result.Users != 7 || result.RegionMappings != 6 || result.RebuildTasks != 8 || result.Banners != 2 || result.SplashScreens != 3 || result.RoomListEntries != 4 || result.RoomSnapshots != 5 {
		t.Fatalf("unexpected result counts: %+v", result)
	}
	if store.renamedAppCode != "lalu" || store.renamedOld != "PH" || store.renamedNew != "PHL" {
		t.Fatalf("admin references not renamed with expected codes: %+v", store)
	}
	if room.req.OldCountryCode != "PH" || room.req.NewCountryCode != "PHL" || room.req.AdminID != 7 {
		t.Fatalf("room rename request mismatch: %+v", room.req)
	}
}

func TestHandleCountryCodeRenameJobIsIdempotentAfterUserDBAlreadyRenamed(t *testing.T) {
	db, mock, closeDB := newCountryRenameMockDB(t)
	defer closeDB()
	store := &fakeCountryRenameJobStore{
		adminCounts: repository.CountryCodeAdminReferenceCounts{Banners: 1, SplashScreens: 1},
	}
	room := &fakeCountryRenameRoomClient{
		result: roomclient.RenameOwnerCountryCodeResult{RoomListEntries: 1, RoomSnapshots: 1},
	}
	service := NewService(nil, db, store, room)

	mock.ExpectBegin()
	expectCountryRenameLockedCountry(mock, "PHL")
	expectCountryRenameAvailableCode(mock, "PHL")
	mock.ExpectExec("UPDATE countries").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE region_countries").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectExec("UPDATE users").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectExec("UPDATE user_region_rebuild_tasks").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectCommit()

	payload := countryCodeRenameJobPayload{
		AppCode:            "lalu",
		CountryID:          12,
		OldCountryCode:     "PH",
		NewCountryCode:     "PHL",
		CountryName:        "Philippines",
		CountryDisplayName: "菲律宾",
		ActorID:            7,
		RequestID:          "req-1",
	}
	body, _ := json.Marshal(payload)
	resultJSON, err := service.HandleCountryCodeRenameJob(context.Background(), &model.AdminJob{ID: 101, PayloadJSON: string(body)})
	if err != nil {
		t.Fatalf("handle retry job failed: %v", err)
	}
	assertCountryRenameExpectations(t, mock)

	var result countryCodeRenameResult
	if err := json.Unmarshal([]byte(resultJSON), &result); err != nil {
		t.Fatalf("decode result: %v", err)
	}
	if result.Users != 0 || result.RegionMappings != 0 || result.RebuildTasks != 0 || result.Banners != 1 || result.SplashScreens != 1 || result.RoomListEntries != 1 || result.RoomSnapshots != 1 {
		t.Fatalf("unexpected idempotent result counts: %+v", result)
	}
}

func validRenameCountryCodeRequest(countryCode string) renameCountryCodeRequest {
	return renameCountryCodeRequest{
		CountryName:        "Philippines",
		CountryCode:        countryCode,
		CountryDisplayName: "菲律宾",
		ISOAlpha3:          "PHL",
		ISONumeric:         "608",
		PhoneCountryCode:   "+63",
		Flag:               "PH",
		SortOrder:          1770,
	}
}

func newCountryRenameMockDB(t *testing.T) (*sql.DB, sqlmock.Sqlmock, func()) {
	t.Helper()
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sqlmock: %v", err)
	}
	return db, mock, func() {
		_ = db.Close()
	}
}

func expectCountryRenameLockedCountry(mock sqlmock.Sqlmock, countryCode string) {
	mock.ExpectQuery(`(?s)SELECT country_id, country_code.*FROM countries.*FOR UPDATE`).
		WithArgs("lalu", int64(12)).
		WillReturnRows(sqlmock.NewRows([]string{"country_id", "country_code"}).AddRow(12, countryCode))
}

func expectCountryRenameDuplicateCode(mock sqlmock.Sqlmock, countryCode string, existingID int64) {
	mock.ExpectQuery(`(?s)SELECT country_id.*FROM countries.*country_id <>`).
		WithArgs("lalu", countryCode, int64(12)).
		WillReturnRows(sqlmock.NewRows([]string{"country_id"}).AddRow(existingID))
}

func expectCountryRenameAvailableCode(mock sqlmock.Sqlmock, countryCode string) {
	mock.ExpectQuery(`(?s)SELECT country_id.*FROM countries.*country_id <>`).
		WithArgs("lalu", countryCode, int64(12)).
		WillReturnError(sql.ErrNoRows)
}

func assertCountryRenameExpectations(t *testing.T, mock sqlmock.Sqlmock) {
	t.Helper()
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations: %v", err)
	}
}

type fakeCountryRenameJobStore struct {
	active         []model.AdminJob
	adminCounts    repository.CountryCodeAdminReferenceCounts
	created        []model.AdminJob
	renamedAppCode string
	renamedOld     string
	renamedNew     string
}

func (s *fakeCountryRenameJobStore) CreateJob(job *model.AdminJob) error {
	job.ID = 101
	job.Status = model.JobStatusPending
	s.created = append(s.created, *job)
	return nil
}

func (s *fakeCountryRenameJobStore) ListActiveJobsByType(string) ([]model.AdminJob, error) {
	return s.active, nil
}

func (s *fakeCountryRenameJobStore) RenameCountryCodeAdminReferences(appCode string, oldCountryCode string, newCountryCode string, _ int64) (repository.CountryCodeAdminReferenceCounts, error) {
	s.renamedAppCode = appCode
	s.renamedOld = oldCountryCode
	s.renamedNew = newCountryCode
	return s.adminCounts, nil
}

type fakeCountryRenameRoomClient struct {
	req    roomclient.RenameOwnerCountryCodeRequest
	result roomclient.RenameOwnerCountryCodeResult
}

func (c *fakeCountryRenameRoomClient) RenameOwnerCountryCode(_ context.Context, req roomclient.RenameOwnerCountryCodeRequest) (roomclient.RenameOwnerCountryCodeResult, error) {
	c.req = req
	return c.result, nil
}
