package channelserver

import (
	"errors"
	"testing"

	"go.uber.org/zap"
)

func newTestMailService(mr MailRepo, gr GuildRepo) *MailService {
	logger, _ := zap.NewDevelopment()
	return NewMailService(mr, gr, logger)
}

func newTestGuildService(gr GuildRepo, mr MailRepo) *GuildService {
	logger, _ := zap.NewDevelopment()
	ms := newTestMailService(mr, gr)
	return NewGuildService(gr, ms, nil, logger)
}

func TestGuildService_OperateMember(t *testing.T) {
	tests := []struct {
		name          string
		actorCharID   uint32
		targetCharID  uint32
		action        GuildMemberAction
		guild         *Guild
		membership    *GuildMember
		acceptErr     error
		rejectErr     error
		removeErr     error
		sendErr       error
		wantErr       bool
		wantErrIs     error
		wantAccepted  uint32
		wantRejected  uint32
		wantRemoved   uint32
		wantMailCount int
		wantRecipient uint32
		wantMailSubj  string
	}{
		{
			name:          "accept application as leader",
			actorCharID:   1,
			targetCharID:  42,
			action:        GuildMemberActionAccept,
			guild:         &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
			membership:    &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
			wantAccepted:  42,
			wantMailCount: 1,
			wantRecipient: 42,
			wantMailSubj:  "Accepted!",
		},
		{
			name:          "reject application as sub-leader",
			actorCharID:   2,
			targetCharID:  42,
			action:        GuildMemberActionReject,
			guild:         &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
			membership:    &GuildMember{GuildID: 10, CharID: 2, OrderIndex: 2}, // sub-leader
			wantRejected:  42,
			wantMailCount: 1,
			wantRecipient: 42,
			wantMailSubj:  "Rejected",
		},
		{
			name:          "kick member as leader",
			actorCharID:   1,
			targetCharID:  42,
			action:        GuildMemberActionKick,
			guild:         &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
			membership:    &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
			wantRemoved:   42,
			wantMailCount: 1,
			wantRecipient: 42,
			wantMailSubj:  "Kicked",
		},
		{
			name:         "unauthorized - not leader or sub",
			actorCharID:  5,
			targetCharID: 42,
			action:       GuildMemberActionAccept,
			guild:        &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
			membership:   &GuildMember{GuildID: 10, CharID: 5, OrderIndex: 10},
			wantErr:      true,
			wantErrIs:    ErrUnauthorized,
		},
		{
			name:         "repo error on accept",
			actorCharID:  1,
			targetCharID: 42,
			action:       GuildMemberActionAccept,
			guild:        &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
			membership:   &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
			acceptErr:    errors.New("db error"),
			wantErr:      true,
		},
		{
			name:          "mail error is best-effort",
			actorCharID:   1,
			targetCharID:  42,
			action:        GuildMemberActionAccept,
			guild:         &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
			membership:    &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
			sendErr:       errors.New("mail failed"),
			wantAccepted:  42,
			wantMailCount: 1,
			wantRecipient: 42,
			wantMailSubj:  "Accepted!",
		},
		{
			name:         "unknown action",
			actorCharID:  1,
			targetCharID: 42,
			action:       GuildMemberAction(99),
			guild:        &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 1}},
			membership:   &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
			wantErr:      true,
			wantErrIs:    ErrUnknownAction,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			guildMock := &mockGuildRepo{
				membership: tt.membership,
				acceptErr:  tt.acceptErr,
				rejectErr:  tt.rejectErr,
				removeErr:  tt.removeErr,
			}
			guildMock.guild = tt.guild
			mailMock := &mockMailRepo{sendErr: tt.sendErr}

			svc := newTestGuildService(guildMock, mailMock)

			result, err := svc.OperateMember(tt.actorCharID, tt.targetCharID, tt.action)

			if tt.wantErr {
				if err == nil {
					t.Fatal("Expected error, got nil")
				}
				if tt.wantErrIs != nil && !errors.Is(err, tt.wantErrIs) {
					t.Errorf("Expected error %v, got %v", tt.wantErrIs, err)
				}
				return
			}
			if err != nil {
				t.Fatalf("Unexpected error: %v", err)
			}

			if tt.wantAccepted != 0 && guildMock.acceptedCharID != tt.wantAccepted {
				t.Errorf("acceptedCharID = %d, want %d", guildMock.acceptedCharID, tt.wantAccepted)
			}
			if tt.wantRejected != 0 && guildMock.rejectedCharID != tt.wantRejected {
				t.Errorf("rejectedCharID = %d, want %d", guildMock.rejectedCharID, tt.wantRejected)
			}
			if tt.wantRemoved != 0 && guildMock.removedCharID != tt.wantRemoved {
				t.Errorf("removedCharID = %d, want %d", guildMock.removedCharID, tt.wantRemoved)
			}
			if len(mailMock.sentMails) != tt.wantMailCount {
				t.Fatalf("sentMails count = %d, want %d", len(mailMock.sentMails), tt.wantMailCount)
			}
			if tt.wantMailCount > 0 {
				if mailMock.sentMails[0].recipientID != tt.wantRecipient {
					t.Errorf("mail recipientID = %d, want %d", mailMock.sentMails[0].recipientID, tt.wantRecipient)
				}
				if mailMock.sentMails[0].subject != tt.wantMailSubj {
					t.Errorf("mail subject = %q, want %q", mailMock.sentMails[0].subject, tt.wantMailSubj)
				}
			}
			if result.MailRecipientID != tt.targetCharID {
				t.Errorf("result.MailRecipientID = %d, want %d", result.MailRecipientID, tt.targetCharID)
			}
		})
	}
}

func TestGuildService_Disband(t *testing.T) {
	tests := []struct {
		name        string
		actorCharID uint32
		guild       *Guild
		disbandErr  error
		wantSuccess bool
		wantDisbID  uint32
	}{
		{
			name:        "leader disbands successfully",
			actorCharID: 1,
			guild:       &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
			wantSuccess: true,
			wantDisbID:  10,
		},
		{
			name:        "non-leader cannot disband",
			actorCharID: 5,
			guild:       &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
			wantSuccess: false,
		},
		{
			name:        "repo error returns failure",
			actorCharID: 1,
			guild:       &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
			disbandErr:  errors.New("db error"),
			wantSuccess: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			guildMock := &mockGuildRepo{disbandErr: tt.disbandErr}
			guildMock.guild = tt.guild
			svc := newTestGuildService(guildMock, &mockMailRepo{})

			result, err := svc.Disband(tt.actorCharID, tt.guild.ID)
			if err != nil {
				t.Fatalf("Unexpected error: %v", err)
			}
			if result.Success != tt.wantSuccess {
				t.Errorf("Success = %v, want %v", result.Success, tt.wantSuccess)
			}
			if tt.wantDisbID != 0 && guildMock.disbandedID != tt.wantDisbID {
				t.Errorf("disbandedID = %d, want %d", guildMock.disbandedID, tt.wantDisbID)
			}
		})
	}
}

func TestGuildService_ResignLeadership(t *testing.T) {
	tests := []struct {
		name           string
		actorCharID    uint32
		guild          *Guild
		members        []*GuildMember
		getMembersErr  error
		wantNewLeader  uint32
		wantErr        bool
		wantSavedCount int
		wantGuildSaved bool
	}{
		{
			name:        "transfers to next eligible member",
			actorCharID: 1,
			guild:       &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
			members: []*GuildMember{
				{CharID: 1, OrderIndex: 1, IsLeader: true},
				{CharID: 2, OrderIndex: 2, AvoidLeadership: false},
			},
			wantNewLeader:  2,
			wantSavedCount: 2,
			wantGuildSaved: true,
		},
		{
			name:        "skips members avoiding leadership",
			actorCharID: 1,
			guild:       &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
			members: []*GuildMember{
				{CharID: 1, OrderIndex: 1, IsLeader: true},
				{CharID: 2, OrderIndex: 2, AvoidLeadership: true},
				{CharID: 3, OrderIndex: 3, AvoidLeadership: false},
			},
			wantNewLeader:  3,
			wantSavedCount: 2,
			wantGuildSaved: true,
		},
		{
			name:        "no eligible successor returns zero",
			actorCharID: 1,
			guild:       &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
			members: []*GuildMember{
				{CharID: 1, OrderIndex: 1, IsLeader: true},
				{CharID: 2, OrderIndex: 2, AvoidLeadership: true},
			},
			wantNewLeader: 0,
		},
		{
			name:          "get members error",
			actorCharID:   1,
			guild:         &Guild{ID: 10, GuildLeader: GuildLeader{LeaderCharID: 1}},
			getMembersErr: errors.New("db error"),
			wantErr:       true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			guildMock := &mockGuildRepo{getMembersErr: tt.getMembersErr}
			guildMock.guild = tt.guild
			guildMock.members = tt.members
			svc := newTestGuildService(guildMock, &mockMailRepo{})

			result, err := svc.ResignLeadership(tt.actorCharID, tt.guild.ID)
			if tt.wantErr {
				if err == nil {
					t.Fatal("Expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Fatalf("Unexpected error: %v", err)
			}
			if result.NewLeaderCharID != tt.wantNewLeader {
				t.Errorf("NewLeaderCharID = %d, want %d", result.NewLeaderCharID, tt.wantNewLeader)
			}
			if tt.wantSavedCount > 0 && len(guildMock.savedMembers) != tt.wantSavedCount {
				t.Errorf("savedMembers count = %d, want %d", len(guildMock.savedMembers), tt.wantSavedCount)
			}
			if tt.wantGuildSaved && guildMock.savedGuild == nil {
				t.Error("Guild should be saved")
			}
		})
	}
}

func TestGuildService_Leave(t *testing.T) {
	tests := []struct {
		name          string
		isApplicant   bool
		rejectErr     error
		removeErr     error
		sendErr       error
		wantSuccess   bool
		wantRejected  uint32
		wantRemoved   uint32
		wantMailCount int
	}{
		{
			name:          "member leaves successfully",
			isApplicant:   false,
			wantSuccess:   true,
			wantRemoved:   1,
			wantMailCount: 1,
		},
		{
			name:          "applicant withdraws via reject",
			isApplicant:   true,
			wantSuccess:   true,
			wantRejected:  1,
			wantMailCount: 1,
		},
		{
			name:        "remove error returns failure",
			isApplicant: false,
			removeErr:   errors.New("db error"),
			wantSuccess: false,
		},
		{
			name:          "mail error is best-effort",
			isApplicant:   false,
			sendErr:       errors.New("mail failed"),
			wantSuccess:   true,
			wantRemoved:   1,
			wantMailCount: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			guildMock := &mockGuildRepo{
				rejectErr: tt.rejectErr,
				removeErr: tt.removeErr,
			}
			guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
			mailMock := &mockMailRepo{sendErr: tt.sendErr}
			svc := newTestGuildService(guildMock, mailMock)

			result, _ := svc.Leave(1, 10, tt.isApplicant, "TestGuild")
			if result.Success != tt.wantSuccess {
				t.Errorf("Success = %v, want %v", result.Success, tt.wantSuccess)
			}
			if tt.wantRejected != 0 && guildMock.rejectedCharID != tt.wantRejected {
				t.Errorf("rejectedCharID = %d, want %d", guildMock.rejectedCharID, tt.wantRejected)
			}
			if tt.wantRemoved != 0 && guildMock.removedCharID != tt.wantRemoved {
				t.Errorf("removedCharID = %d, want %d", guildMock.removedCharID, tt.wantRemoved)
			}
			if len(mailMock.sentMails) != tt.wantMailCount {
				t.Errorf("sentMails count = %d, want %d", len(mailMock.sentMails), tt.wantMailCount)
			}
		})
	}
}

func TestGuildService_PostScout(t *testing.T) {
	strings := ScoutInviteStrings{Title: "Invite", Body: "Join 「%s」"}

	tests := []struct {
		name         string
		membership   *GuildMember
		guild        *Guild
		hasInvite    bool
		hasInviteErr error
		createAppErr error
		getMemberErr error
		wantErr      error
	}{
		{
			name:       "successful scout",
			membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
			guild:      &Guild{ID: 10, Name: "TestGuild"},
		},
		{
			name:       "already invited",
			membership: &GuildMember{GuildID: 10, CharID: 1, IsLeader: true, OrderIndex: 1},
			guild:      &Guild{ID: 10, Name: "TestGuild"},
			hasInvite:  true,
			wantErr:    ErrAlreadyInvited,
		},
		{
			name:       "cannot recruit",
			membership: &GuildMember{GuildID: 10, CharID: 1, OrderIndex: 10}, // not recruiter, not sub-leader
			guild:      &Guild{ID: 10, Name: "TestGuild"},
			wantErr:    ErrCannotRecruit,
		},
		{
			name:         "nil membership",
			getMemberErr: errors.New("not found"),
			guild:        &Guild{ID: 10, Name: "TestGuild"},
			wantErr:      errors.New("any"), // just check err != nil
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			guildMock := &mockGuildRepo{
				membership:      tt.membership,
				hasInviteResult: tt.hasInvite,
				hasInviteErr:    tt.hasInviteErr,
				createAppErr:    tt.createAppErr,
				getMemberErr:    tt.getMemberErr,
			}
			guildMock.guild = tt.guild
			svc := newTestGuildService(guildMock, &mockMailRepo{})

			err := svc.PostScout(1, 42, strings)

			if tt.wantErr != nil {
				if err == nil {
					t.Fatal("Expected error, got nil")
				}
				if errors.Is(tt.wantErr, ErrAlreadyInvited) || errors.Is(tt.wantErr, ErrCannotRecruit) {
					if !errors.Is(err, tt.wantErr) {
						t.Errorf("Expected %v, got %v", tt.wantErr, err)
					}
				}
				return
			}
			if err != nil {
				t.Fatalf("Unexpected error: %v", err)
			}
		})
	}
}

func TestGuildService_AnswerScout(t *testing.T) {
	strings := AnswerScoutStrings{
		SuccessTitle:  "Success!",
		SuccessBody:   "Joined 「%s」.",
		AcceptedTitle: "Accepted",
		AcceptedBody:  "Accepted invite to 「%s」.",
		RejectedTitle: "Rejected",
		RejectedBody:  "Rejected invite to 「%s」.",
		DeclinedTitle: "Declined",
		DeclinedBody:  "Declined invite to 「%s」.",
	}

	tests := []struct {
		name          string
		accept        bool
		guild         *Guild
		hasInvite     bool
		acceptErr     error
		rejectErr     error
		sendErr       error
		getErr        error
		wantSuccess   bool
		wantErr       error
		wantMailCount int
		wantAccepted  uint32
		wantDeclined  uint32
	}{
		{
			name:          "accept invitation",
			accept:        true,
			guild:         &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
			hasInvite:     true,
			wantSuccess:   true,
			wantMailCount: 2,
			wantAccepted:  1,
		},
		{
			name:          "decline invitation",
			accept:        false,
			guild:         &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
			hasInvite:     true,
			wantSuccess:   true,
			wantMailCount: 2,
			wantDeclined:  1,
		},
		{
			name:        "application missing",
			accept:      true,
			guild:       &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
			hasInvite:   false,
			wantSuccess: false,
			wantErr:     ErrApplicationMissing,
		},
		{
			name:    "guild not found",
			accept:  true,
			guild:   &Guild{ID: 10, Name: "TestGuild"},
			getErr:  errors.New("not found"),
			wantErr: errors.New("any"),
		},
		{
			name:          "mail error is best-effort",
			accept:        true,
			guild:         &Guild{ID: 10, Name: "TestGuild", GuildLeader: GuildLeader{LeaderCharID: 50}},
			hasInvite:     true,
			sendErr:       errors.New("mail failed"),
			wantSuccess:   true,
			wantMailCount: 2,
			wantAccepted:  1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			guildMock := &mockGuildRepo{
				hasInviteResult: tt.hasInvite,
				acceptErr:       tt.acceptErr,
				rejectErr:       tt.rejectErr,
			}
			guildMock.guild = tt.guild
			guildMock.getErr = tt.getErr
			mailMock := &mockMailRepo{sendErr: tt.sendErr}
			svc := newTestGuildService(guildMock, mailMock)

			result, err := svc.AnswerScout(1, 50, tt.accept, strings)

			if tt.wantErr != nil {
				if err == nil {
					t.Fatal("Expected error, got nil")
				}
				if errors.Is(tt.wantErr, ErrApplicationMissing) && !errors.Is(err, ErrApplicationMissing) {
					t.Errorf("Expected ErrApplicationMissing, got %v", err)
				}
				if result != nil && result.Success {
					t.Error("Result should not be successful")
				}
				return
			}
			if err != nil {
				t.Fatalf("Unexpected error: %v", err)
			}
			if result.Success != tt.wantSuccess {
				t.Errorf("Success = %v, want %v", result.Success, tt.wantSuccess)
			}
			if len(mailMock.sentMails) != tt.wantMailCount {
				t.Errorf("sentMails count = %d, want %d", len(mailMock.sentMails), tt.wantMailCount)
			}
			if tt.wantAccepted != 0 && guildMock.acceptInviteCharID != tt.wantAccepted {
				t.Errorf("acceptInviteCharID = %d, want %d", guildMock.acceptInviteCharID, tt.wantAccepted)
			}
			if tt.wantDeclined != 0 && guildMock.declineInviteCharID != tt.wantDeclined {
				t.Errorf("declineInviteCharID = %d, want %d", guildMock.declineInviteCharID, tt.wantDeclined)
			}
		})
	}
}
