package auth

import (
	"context"
	stderrors "errors"
	"strings"
	"testing"
	"time"

	"github.com/nexuscrm/kernel/pkg/ports"
	"github.com/nexuscrm/shared/pkg/models"
)

type disconnectAuditRecorderStub struct {
	action  string
	section string
	display string
	userID  string
	err     error
}

func (s *disconnectAuditRecorderStub) RecordSetupChange(_ context.Context, action, section, display, userID string) error {
	s.action, s.section, s.display, s.userID = action, section, display, userID
	return s.err
}

func TestListUserConnectionsReadsOnlyLiveAuthorizationGrantProjection(t *testing.T) {
	lastUsedAt := time.Date(2026, 7, 27, 12, 0, 0, 0, time.UTC)
	repo := &oauthRepoStub{connections: []ports.AuthorizationGrantConnection{{
		ClientID: "client-1", ClientName: "External App", Scope: "records.read",
		FirstConnectedAt: time.Date(2026, 7, 26, 12, 0, 0, 0, time.UTC),
		LastUsedAt:       &lastUsedAt, PolicyVersion: 3,
	}}}
	svc := NewOAuthService(nil, nil, nil, nil, repo)

	connections, err := svc.ListUserConnections(context.Background(), "user-1")
	if err != nil {
		t.Fatalf("ListUserConnections: %v", err)
	}
	if len(connections) != 1 || connections[0].ClientID != "client-1" || connections[0].ClientName != "External App" {
		t.Fatalf("connections = %#v, want the grant projection", connections)
	}
}

func TestDisconnectUserConnectionRevokesGrantThenUserClientCredentials(t *testing.T) {
	clientID := "client-1"
	repo := &oauthRepoStub{refreshByHash: map[string]*models.SystemOAuthRefreshToken{
		"refresh-1": {ID: "refresh-1", UserID: "user-1", ConnectedAppID: &clientID},
	}}
	sessions := &sessionRepoStub{}
	svc := NewOAuthService(nil, &systemRepoStub{}, sessions, nil, repo)
	audit := &disconnectAuditRecorderStub{}
	svc.SetDisconnectAuditRecorder(audit)

	if err := svc.DisconnectUserConnection(context.Background(), "user-1", clientID); err != nil {
		t.Fatalf("DisconnectUserConnection: %v", err)
	}
	if !repo.grantInactive || repo.disconnectedSubjectID != "user-1" || repo.disconnectedClientID != clientID || repo.disconnectedRevokedBy != "user:disconnect" {
		t.Fatalf("authorization grant revoke = inactive:%t subject:%q client:%q by:%q", repo.grantInactive, repo.disconnectedSubjectID, repo.disconnectedClientID, repo.disconnectedRevokedBy)
	}
	if !repo.refreshByHash["refresh-1"].IsRevoked {
		t.Fatal("user/client refresh token remains live after disconnect")
	}
	if sessions.disconnectedUserID != "user-1" || sessions.disconnectedClientID != clientID {
		t.Fatalf("session revoke = (%q,%q), want (user-1,%s)", sessions.disconnectedUserID, sessions.disconnectedClientID, clientID)
	}
	if repo.consumedUnboundCodesSubjectID != "user-1" || repo.consumedUnboundCodesLockingClientID != clientID {
		t.Fatalf("unbound-code sweep = subject:%q locking_client:%q", repo.consumedUnboundCodesSubjectID, repo.consumedUnboundCodesLockingClientID)
	}
	if audit.action != "Disconnect OAuth connection" || audit.section != "Connected App" || audit.userID != "user-1" ||
		!strings.Contains(audit.display, "actor=user-1 target_user=user-1 connected_app=client-1 revoked_grants=1 consumed_unbound_codes=1 revoked_refresh_tokens=1 revoked_sessions=1") {
		t.Fatalf("disconnect audit = %#v", audit)
	}
}

func TestDisconnectUserConnectionFailsClosedWhenAuditFails(t *testing.T) {
	clientID := "client-1"
	repo := &oauthRepoStub{}
	svc := NewOAuthService(nil, &systemRepoStub{}, &sessionRepoStub{}, nil, repo)
	svc.SetDisconnectAuditRecorder(&disconnectAuditRecorderStub{err: stderrors.New("audit unavailable")})
	if err := svc.DisconnectUserConnection(context.Background(), "user-1", clientID); err == nil || !strings.Contains(err.Error(), "audit unavailable") {
		t.Fatalf("DisconnectUserConnection audit failure = %v, want propagated failure", err)
	}
}

func TestDisconnectUserConnectionRejectsMissingIdentifiers(t *testing.T) {
	svc := NewOAuthService(nil, nil, nil, nil, &oauthRepoStub{})
	if err := svc.DisconnectUserConnection(context.Background(), "", "client-1"); err == nil {
		t.Fatal("empty user id succeeded")
	}
	if err := svc.DisconnectUserConnection(context.Background(), "user-1", ""); err == nil {
		t.Fatal("empty connected app id succeeded")
	}
}
