package service

import (
	"context"
	"encoding/base64"
	"errors"
	"testing"

	"wucher/internal/config"
	mastersettings "wucher/internal/domain/master_settings"
)

type masterSettingsRepoMock struct {
	createErr error
	updateErr error
	deleteErr error
	getErr    error
	listErr   error

	lastCreate     *mastersettings.MasterSettings
	lastUpdate     *mastersettings.MasterSettings
	lastDeleteID   []byte
	lastDeleteBy   []byte
	lastGetID      []byte
	lastListSort   string
	lastListFilter string
	lastListLimit  int
	lastListOffset int

	getRow    *mastersettings.MasterSettings
	listRows  []mastersettings.MasterSettings
	listTotal int64

	lastSettingRows []mastersettings.MasterSettingValue
	lastSettingKeys []string
	settingRows     []mastersettings.MasterSettingValue
	settingErr      error
}

func (m *masterSettingsRepoMock) Create(_ context.Context, row *mastersettings.MasterSettings) error {
	m.lastCreate = row
	return m.createErr
}

func (m *masterSettingsRepoMock) Update(_ context.Context, row *mastersettings.MasterSettings) error {
	m.lastUpdate = row
	return m.updateErr
}

func (m *masterSettingsRepoMock) Delete(_ context.Context, id []byte, deletedBy []byte) error {
	m.lastDeleteID = append([]byte(nil), id...)
	m.lastDeleteBy = append([]byte(nil), deletedBy...)
	return m.deleteErr
}

func (m *masterSettingsRepoMock) GetByID(_ context.Context, id []byte) (*mastersettings.MasterSettings, error) {
	m.lastGetID = append([]byte(nil), id...)
	return m.getRow, m.getErr
}

func (m *masterSettingsRepoMock) List(_ context.Context, filter, sort string, limit, offset int) ([]mastersettings.MasterSettings, int64, error) {
	m.lastListFilter = filter
	m.lastListSort = sort
	m.lastListLimit = limit
	m.lastListOffset = offset
	return m.listRows, m.listTotal, m.listErr
}

func (m *masterSettingsRepoMock) UpsertSettingValues(_ context.Context, rows []mastersettings.MasterSettingValue) error {
	m.lastSettingRows = append([]mastersettings.MasterSettingValue(nil), rows...)
	return m.settingErr
}

func (m *masterSettingsRepoMock) GetSettingValuesByKeys(_ context.Context, keys []string) ([]mastersettings.MasterSettingValue, error) {
	m.lastSettingKeys = append([]string(nil), keys...)
	return append([]mastersettings.MasterSettingValue(nil), m.settingRows...), m.settingErr
}

func TestNewMasterSettingsService(t *testing.T) {
	svc := NewMasterSettingsService(&masterSettingsRepoMock{}, MasterSettingsServiceDependencies{})
	if svc == nil || svc.repo == nil {
		t.Fatalf("expected service initialized")
	}
}

func TestMasterSettingsServiceCreateUpdateDeleteGetByID(t *testing.T) {
	ctx := context.Background()
	row := &mastersettings.MasterSettings{CompanyName: "Wucher"}
	id := []byte("1234567890123456")
	actor := []byte("abcdefghijklmnop")

	t.Run("create success and error", func(t *testing.T) {
		repo := &masterSettingsRepoMock{}
		svc := NewMasterSettingsService(repo, MasterSettingsServiceDependencies{})
		if err := svc.Create(ctx, row); err != nil {
			t.Fatalf("expected no error, got %v", err)
		}
		if repo.lastCreate != row {
			t.Fatalf("expected row passed to repo")
		}

		repo.createErr = errors.New("create failed")
		if err := svc.Create(ctx, row); err == nil {
			t.Fatalf("expected create error")
		}
	})

	t.Run("update success and error", func(t *testing.T) {
		repo := &masterSettingsRepoMock{}
		svc := NewMasterSettingsService(repo, MasterSettingsServiceDependencies{})
		if err := svc.Update(ctx, row); err != nil {
			t.Fatalf("expected no error, got %v", err)
		}
		if repo.lastUpdate != row {
			t.Fatalf("expected row passed to repo")
		}

		repo.updateErr = errors.New("update failed")
		if err := svc.Update(ctx, row); err == nil {
			t.Fatalf("expected update error")
		}
	})

	t.Run("delete success and error", func(t *testing.T) {
		repo := &masterSettingsRepoMock{}
		svc := NewMasterSettingsService(repo, MasterSettingsServiceDependencies{})
		if err := svc.Delete(ctx, id, actor); err != nil {
			t.Fatalf("expected no error, got %v", err)
		}
		if string(repo.lastDeleteID) != string(id) || string(repo.lastDeleteBy) != string(actor) {
			t.Fatalf("expected id and deletedBy passed to repo")
		}

		repo.deleteErr = errors.New("delete failed")
		if err := svc.Delete(ctx, id, actor); err == nil {
			t.Fatalf("expected delete error")
		}
	})

	t.Run("get by id success and error", func(t *testing.T) {
		repo := &masterSettingsRepoMock{getRow: row}
		svc := NewMasterSettingsService(repo, MasterSettingsServiceDependencies{})
		got, err := svc.GetByID(ctx, id)
		if err != nil {
			t.Fatalf("expected no error, got %v", err)
		}
		if got != row {
			t.Fatalf("expected repo row returned")
		}
		if string(repo.lastGetID) != string(id) {
			t.Fatalf("expected id passed to repo")
		}

		repo.getErr = errors.New("get failed")
		if _, err := svc.GetByID(ctx, id); err == nil {
			t.Fatalf("expected get error")
		}
	})
}

func TestMasterSettingsServiceList(t *testing.T) {
	ctx := context.Background()
	repo := &masterSettingsRepoMock{
		listRows:  []mastersettings.MasterSettings{{CompanyName: "Wucher"}},
		listTotal: 1,
	}
	svc := NewMasterSettingsService(repo, MasterSettingsServiceDependencies{})

	rows, total, err := svc.List(ctx, "wu", "-updated_at", 10, 5)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if len(rows) != 1 || total != 1 {
		t.Fatalf("unexpected list result: len=%d total=%d", len(rows), total)
	}
	if repo.lastListSort != "updated_at DESC" {
		t.Fatalf("expected normalized sort, got %q", repo.lastListSort)
	}
	if repo.lastListFilter != "wu" || repo.lastListLimit != 10 || repo.lastListOffset != 5 {
		t.Fatalf("unexpected list params passed to repo")
	}

	repo.listErr = errors.New("list failed")
	if _, _, err := svc.List(ctx, "", "unknown", 0, 0); err == nil {
		t.Fatalf("expected list error")
	}
	if repo.lastListSort != "" {
		t.Fatalf("expected unknown sort normalized to empty string, got %q", repo.lastListSort)
	}
}

func TestNormalizeMasterSettingsSort(t *testing.T) {
	tests := []struct {
		name string
		in   string
		want string
	}{
		{name: "company asc", in: "company_name", want: "company_name ASC"},
		{name: "company desc", in: "-company_name", want: "company_name DESC"},
		{name: "title asc", in: "title", want: "title ASC"},
		{name: "title desc", in: "-title", want: "title DESC"},
		{name: "subtitle asc", in: "subtitle", want: "subtitle ASC"},
		{name: "subtitle desc", in: "-subtitle", want: "subtitle DESC"},
		{name: "logo url asc", in: "logo_url", want: "logo_url ASC"},
		{name: "logo url desc", in: "-logo_url", want: "logo_url DESC"},
		{name: "logo file asc", in: "logo_file_uuid", want: "logo_file_id ASC"},
		{name: "logo file desc", in: "-logo_file_uuid", want: "logo_file_id DESC"},
		{name: "created asc", in: "created_at", want: "created_at ASC"},
		{name: "created desc", in: "-created_at", want: "created_at DESC"},
		{name: "updated asc", in: "updated_at", want: "updated_at ASC"},
		{name: "updated desc", in: "-updated_at", want: "updated_at DESC"},
		{name: "unknown", in: "x", want: ""},
		{name: "empty", in: "", want: ""},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := normalizeMasterSettingsSort(tc.in)
			if got != tc.want {
				t.Fatalf("normalize sort %q: got %q want %q", tc.in, got, tc.want)
			}
		})
	}
}

func TestMasterSettingsServiceMicrosoftEntra(t *testing.T) {
	ctx := context.Background()
	repo := &masterSettingsRepoMock{}
	svc := NewMasterSettingsService(repo, MasterSettingsServiceDependencies{})

	keyB64 := "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
	protector, err := NewAESGCMProtectorFromBase64(keyB64)
	if err != nil {
		t.Fatalf("new protector: %v", err)
	}
	svc.SetSecretProtector(protector)

	repo.settingRows = []mastersettings.MasterSettingValue{
		{SettingKey: mastersettings.SettingMicrosoftEntraTenantID, Value: "tenant"},
		{SettingKey: mastersettings.SettingMicrosoftEntraClientID, Value: "client"},
		{SettingKey: mastersettings.SettingMicrosoftEntraClientSecret, Value: "cipher", IsEncrypted: true},
		{SettingKey: mastersettings.SettingMicrosoftEntraRedirectURL, Value: "https://app.example.com/callback"},
		{SettingKey: mastersettings.SettingMicrosoftEntraAuthority, Value: "https://login.microsoftonline.com/"},
		{SettingKey: mastersettings.SettingMicrosoftEntraScopes, Value: "openid,profile,email"},
	}

	got, err := svc.UpsertMicrosoftEntraConfig(ctx, mastersettings.MicrosoftEntraConfigInput{
		TenantID:     " tenant ",
		ClientID:     " client ",
		ClientSecret: "super-secret",
		RedirectURL:  "https://app.example.com/callback",
		Authority:    "https://login.microsoftonline.com",
		Scopes:       "openid, profile,email,profile",
	}, []byte("abcdefghijklmnop"))
	if err != nil {
		t.Fatalf("upsert microsoft entra config: %v", err)
	}
	if got == nil || got.ClientSecretMasked != "********" {
		t.Fatalf("expected masked secret in view")
	}
	if len(repo.lastSettingRows) != len(mastersettings.MicrosoftEntraSettingKeys) {
		t.Fatalf("unexpected setting rows len: %d", len(repo.lastSettingRows))
	}

	var encryptedSecret string
	for _, row := range repo.lastSettingRows {
		if row.SettingKey == mastersettings.SettingMicrosoftEntraClientSecret {
			encryptedSecret = row.Value
			if !row.IsEncrypted {
				t.Fatalf("expected secret row encrypted")
			}
		}
	}
	if encryptedSecret == "" {
		t.Fatalf("secret row not written")
	}
	rawCipher, err := base64.StdEncoding.DecodeString(encryptedSecret)
	if err != nil {
		t.Fatalf("decode secret ciphertext: %v", err)
	}
	plain, err := protector.Decrypt(rawCipher)
	if err != nil {
		t.Fatalf("decrypt secret ciphertext: %v", err)
	}
	if string(plain) != "super-secret" {
		t.Fatalf("unexpected decrypted secret: %q", string(plain))
	}

	loaded, err := svc.GetMicrosoftEntraConfig(ctx)
	if err != nil {
		t.Fatalf("get microsoft entra config: %v", err)
	}
	if loaded == nil || loaded.ClientSecretMasked != "********" {
		t.Fatalf("expected masked secret from get")
	}

	t.Run("keep existing secret when input secret is empty", func(t *testing.T) {
		repo.lastSettingRows = nil
		repo.settingRows = []mastersettings.MasterSettingValue{
			{SettingKey: mastersettings.SettingMicrosoftEntraClientSecret, Value: "existing-cipher", IsEncrypted: true},
			{SettingKey: mastersettings.SettingMicrosoftEntraTenantID, Value: "tenant"},
			{SettingKey: mastersettings.SettingMicrosoftEntraClientID, Value: "client"},
			{SettingKey: mastersettings.SettingMicrosoftEntraRedirectURL, Value: "https://app.example.com/callback"},
			{SettingKey: mastersettings.SettingMicrosoftEntraAuthority, Value: "https://login.microsoftonline.com/"},
			{SettingKey: mastersettings.SettingMicrosoftEntraScopes, Value: "openid,profile,email"},
		}

		_, err := svc.UpsertMicrosoftEntraConfig(ctx, mastersettings.MicrosoftEntraConfigInput{
			TenantID:     "tenant",
			ClientID:     "client",
			ClientSecret: " ",
			RedirectURL:  "https://app.example.com/callback",
			Authority:    "https://login.microsoftonline.com/",
			Scopes:       "openid,profile,email",
		}, []byte("abcdefghijklmnop"))
		if err != nil {
			t.Fatalf("upsert with empty secret: %v", err)
		}

		for _, row := range repo.lastSettingRows {
			if row.SettingKey == mastersettings.SettingMicrosoftEntraClientSecret {
				t.Fatalf("expected secret row not updated when input secret is empty")
			}
		}
	})

	t.Run("empty secret rejected on initial setup", func(t *testing.T) {
		repo.lastSettingRows = nil
		repo.settingRows = []mastersettings.MasterSettingValue{}

		_, err := svc.UpsertMicrosoftEntraConfig(ctx, mastersettings.MicrosoftEntraConfigInput{
			TenantID:     "tenant",
			ClientID:     "client",
			ClientSecret: "",
			RedirectURL:  "https://app.example.com/callback",
			Authority:    "https://login.microsoftonline.com/",
			Scopes:       "openid,profile,email",
		}, []byte("abcdefghijklmnop"))
		if err == nil {
			t.Fatalf("expected error when secret empty and no existing value")
		}
		if err.Error() != "MS_ENTRA_CLIENT_SECRET is required for initial setup" {
			t.Fatalf("unexpected error: %v", err)
		}
	})
}

func TestSeedMicrosoftEntraConfigIfMissing(t *testing.T) {
	ctx := context.Background()

	keyB64 := "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
	protector, err := NewAESGCMProtectorFromBase64(keyB64)
	if err != nil {
		t.Fatalf("new protector: %v", err)
	}

	t.Run("seed inserts all keys when missing", func(t *testing.T) {
		repo := &masterSettingsRepoMock{}
		svc := NewMasterSettingsService(repo, MasterSettingsServiceDependencies{Protector: protector})

		err := svc.SeedMicrosoftEntraConfigIfMissing(ctx, config.MicrosoftSSOConfig{
			TenantID:     "tenant",
			ClientID:     "client",
			ClientSecret: "secret",
			RedirectURL:  "https://app.example.com/challenge/msentra",
			Authority:    "https://login.microsoftonline.com/",
			Scopes:       []string{"openid", "profile", "email"},
		})
		if err != nil {
			t.Fatalf("seed microsoft entra: %v", err)
		}
		if len(repo.lastSettingRows) != 3 {
			t.Fatalf("expected 3 rows, got %d", len(repo.lastSettingRows))
		}
	})

	t.Run("seed only inserts missing and preserves existing", func(t *testing.T) {
		repo := &masterSettingsRepoMock{
			settingRows: []mastersettings.MasterSettingValue{
				{SettingKey: mastersettings.SettingMicrosoftEntraTenantID, Value: "existing-tenant"},
				{SettingKey: mastersettings.SettingMicrosoftEntraClientID, Value: "existing-client"},
				{SettingKey: mastersettings.SettingMicrosoftEntraClientSecret, Value: "existing-encrypted", IsEncrypted: true},
			},
		}
		svc := NewMasterSettingsService(repo, MasterSettingsServiceDependencies{Protector: protector})

		err := svc.SeedMicrosoftEntraConfigIfMissing(ctx, config.MicrosoftSSOConfig{
			TenantID:     "tenant",
			ClientID:     "client",
			ClientSecret: "secret",
			RedirectURL:  "https://app.example.com/challenge/msentra",
			Authority:    "https://login.microsoftonline.com",
			Scopes:       []string{"openid", "profile", "email"},
		})
		if err != nil {
			t.Fatalf("seed microsoft entra: %v", err)
		}
		if len(repo.lastSettingRows) != 3 {
			t.Fatalf("expected 3 missing rows to be inserted, got %d", len(repo.lastSettingRows))
		}
		for i := range repo.lastSettingRows {
			if repo.lastSettingRows[i].SettingKey == mastersettings.SettingMicrosoftEntraClientSecret {
				t.Fatalf("client secret should not be overwritten when existing value is present")
			}
		}
	})
}

func TestMicrosoftEntraPartialViewButStrictRuntime(t *testing.T) {
	ctx := context.Background()
	repo := &masterSettingsRepoMock{
		settingRows: []mastersettings.MasterSettingValue{
			{SettingKey: mastersettings.SettingMicrosoftEntraRedirectURL, Value: "https://app.example.com/challenge/msentra"},
			{SettingKey: mastersettings.SettingMicrosoftEntraAuthority, Value: "https://login.microsoftonline.com/"},
			{SettingKey: mastersettings.SettingMicrosoftEntraScopes, Value: "openid,profile,email"},
		},
	}
	svc := NewMasterSettingsService(repo, MasterSettingsServiceDependencies{})

	view, err := svc.GetMicrosoftEntraConfig(ctx)
	if err != nil {
		t.Fatalf("get microsoft entra config: %v", err)
	}
	if view == nil {
		t.Fatalf("expected partial config view, got nil")
	}
	if view.RedirectURL == "" || view.Authority == "" || view.Scopes == "" {
		t.Fatalf("expected non-sensitive fields populated")
	}

	if _, err := svc.LoadMicrosoftEntraRuntimeConfig(ctx); err == nil {
		t.Fatalf("expected strict runtime config to fail when tenant/client/secret missing")
	}
}
