package providertokens

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	patstore "github.com/tidbcloud/AgentTeam/internal/store/providertokens"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type staticLinkedSourceCounter struct {
	counts map[string]int64
}

func (c staticLinkedSourceCounter) CountSourceMirrorsByCredentialRefs(_ context.Context, _ string, credentialRefs []string) (map[string]int64, error) {
	out := make(map[string]int64, len(credentialRefs))
	for _, ref := range credentialRefs {
		out[ref] = c.counts[ref]
	}
	return out, nil
}

func TestServiceStoresCiphertextListsMetadataAndBlocksLinkedRemoval(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&patstore.PersonalAccessToken{}))
	store := patstore.New(db)
	counter := staticLinkedSourceCounter{counts: map[string]int64{}}
	now := time.Date(2026, 7, 2, 10, 0, 0, 0, time.UTC)
	svc := New(store, Options{
		Now:            func() time.Time { return now },
		CredentialsKey: []byte("0123456789abcdef0123456789abcdef"),
		LinkCounter:    counter,
	})

	created, err := svc.Create(ctx, CreateRequest{
		WorkspaceID:      "wsp_1",
		ActorPrincipalID: "usr_1",
		DisplayName:      "GitHub read token",
		ProviderType:     ProviderGitHub,
		ProviderBaseURL:  "GitHub.com/",
		AccessToken:      "ghp_plaintext_fixture",
	})
	require.NoError(t, err)
	require.Equal(t, "https://github.com", created.ProviderBaseURL)
	require.Equal(t, []string{UsageKBMirror}, created.AllowedUsages)

	rows, err := store.ListOwned(ctx, patstore.ListOptions{
		WorkspaceID:       "wsp_1",
		OwnerPrincipalID:  "usr_1",
		IncludeCiphertext: true,
	})
	require.NoError(t, err)
	require.Len(t, rows, 1)
	require.NotEmpty(t, rows[0].ValueCiphertext)
	require.NotEqual(t, "ghp_plaintext_fixture", rows[0].ValueCiphertext)

	metadataRows, err := store.ListOwned(ctx, patstore.ListOptions{
		WorkspaceID:       "wsp_1",
		OwnerPrincipalID:  "usr_1",
		IncludeCiphertext: false,
	})
	require.NoError(t, err)
	require.Len(t, metadataRows, 1)
	require.Empty(t, metadataRows[0].ValueCiphertext)

	counter.counts[created.CredentialRef] = 2
	listed, err := svc.List(ctx, ListRequest{
		WorkspaceID:      "wsp_1",
		ActorPrincipalID: "usr_1",
		Usage:            UsageKBMirror,
		ProviderBaseURL:  "https://github.com",
	})
	require.NoError(t, err)
	require.Len(t, listed.Items, 1)
	require.EqualValues(t, 2, listed.Items[0].LinkedSourceCount)
	require.Equal(t, created.CredentialRef, listed.Items[0].CredentialRef)

	_, err = svc.Revoke(ctx, RevokeRequest{WorkspaceID: "wsp_1", ActorPrincipalID: "usr_1", ID: created.ID})
	require.ErrorIs(t, err, ErrTokenStateConflict)
	err = svc.Delete(ctx, DeleteRequest{WorkspaceID: "wsp_1", ActorPrincipalID: "usr_1", ID: created.ID})
	require.ErrorIs(t, err, ErrTokenStateConflict)

	counter.counts[created.CredentialRef] = 0
	revoked, err := svc.Revoke(ctx, RevokeRequest{WorkspaceID: "wsp_1", ActorPrincipalID: "usr_1", ID: created.ID})
	require.NoError(t, err)
	require.Equal(t, StatusRevoked, revoked.Status)
}

func TestServiceListStatusFilterUsesComputedExpiry(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&patstore.PersonalAccessToken{}))
	now := time.Date(2026, 7, 2, 10, 0, 0, 0, time.UTC)
	svc := New(patstore.New(db), Options{
		Now:            func() time.Time { return now },
		CredentialsKey: []byte("0123456789abcdef0123456789abcdef"),
	})

	activeExpiresAt := now.Add(time.Hour)
	active, err := svc.Create(ctx, CreateRequest{
		WorkspaceID:      "wsp_1",
		ActorPrincipalID: "usr_1",
		DisplayName:      "Active token",
		ProviderType:     ProviderGitHub,
		ProviderBaseURL:  "https://github.com",
		AccessToken:      "active-token",
		ExpiresAt:        &activeExpiresAt,
	})
	require.NoError(t, err)
	expiredAt := now.Add(-time.Hour)
	expired, err := svc.Create(ctx, CreateRequest{
		WorkspaceID:      "wsp_1",
		ActorPrincipalID: "usr_1",
		DisplayName:      "Expired token",
		ProviderType:     ProviderGitHub,
		ProviderBaseURL:  "https://github.com",
		AccessToken:      "expired-token",
		ExpiresAt:        &expiredAt,
	})
	require.NoError(t, err)

	activeList, err := svc.List(ctx, ListRequest{WorkspaceID: "wsp_1", ActorPrincipalID: "usr_1", Status: StatusActive})
	require.NoError(t, err)
	require.Len(t, activeList.Items, 1)
	require.Equal(t, active.ID, activeList.Items[0].ID)
	require.Equal(t, StatusActive, activeList.Items[0].Status)

	expiredList, err := svc.List(ctx, ListRequest{WorkspaceID: "wsp_1", ActorPrincipalID: "usr_1", Status: StatusExpired})
	require.NoError(t, err)
	require.Len(t, expiredList.Items, 1)
	require.Equal(t, expired.ID, expiredList.Items[0].ID)
	require.Equal(t, StatusExpired, expiredList.Items[0].Status)
}

func TestProviderBaseURLFromRemoteURLNormalizesHTTPAndSCPLikeRemotes(t *testing.T) {
	tests := []struct {
		name    string
		remote  string
		want    string
		wantErr bool
	}{
		{
			name:   "https remote",
			remote: "https://GitHub.com/pingcap/agentteam.git",
			want:   "https://github.com",
		},
		{
			name:   "http remote",
			remote: "http://git.example.com/org/repo.git",
			want:   "http://git.example.com",
		},
		{
			name:   "scp-like remote",
			remote: "git@GitHub.com:pingcap/agentteam.git",
			want:   "https://github.com",
		},
		{
			name:    "unsupported remote",
			remote:  "not-a-remote",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := ProviderBaseURLFromRemoteURL(tt.remote)
			if tt.wantErr {
				if !errors.Is(err, ErrInvalidRequest) {
					t.Fatalf("expected invalid request, got %v", err)
				}
				return
			}
			if err != nil {
				t.Fatalf("ProviderBaseURLFromRemoteURL() error = %v", err)
			}
			if got != tt.want {
				t.Fatalf("ProviderBaseURLFromRemoteURL() = %q, want %q", got, tt.want)
			}
		})
	}
}

func TestRemoteURLMatchesProviderBaseURLHonorsPathBoundary(t *testing.T) {
	tests := []struct {
		name            string
		remoteURL       string
		providerBaseURL string
		want            bool
		wantErr         bool
	}{
		{
			name:            "host-level https base matches same host",
			remoteURL:       "https://git.example.com/org/repo.git",
			providerBaseURL: "https://git.example.com",
			want:            true,
		},
		{
			name:            "path-level https base matches repo below service root",
			remoteURL:       "https://example.com/gitlab/org/repo.git",
			providerBaseURL: "https://example.com/gitlab",
			want:            true,
		},
		{
			name:            "path-level base does not match sibling prefix",
			remoteURL:       "https://example.com/gitlab2/org/repo.git",
			providerBaseURL: "https://example.com/gitlab",
			want:            false,
		},
		{
			name:            "scp-like path-level base matches repo below service root",
			remoteURL:       "git@example.com:gitlab/org/repo.git",
			providerBaseURL: "https://example.com/gitlab",
			want:            true,
		},
		{
			name:            "scheme must match http remotes",
			remoteURL:       "http://git.example.com/org/repo.git",
			providerBaseURL: "https://git.example.com",
			want:            false,
		},
		{
			name:            "invalid remote",
			remoteURL:       "not-a-remote",
			providerBaseURL: "https://git.example.com",
			wantErr:         true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := RemoteURLMatchesProviderBaseURL(tt.remoteURL, tt.providerBaseURL)
			if tt.wantErr {
				require.ErrorIs(t, err, ErrInvalidRequest)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tt.want, got)
		})
	}
}

func TestNormalizeProviderBaseURLAcceptsHostOrHTTPURLOnly(t *testing.T) {
	got, err := NormalizeProviderBaseURL("GitHub.com/")
	if err != nil {
		t.Fatalf("NormalizeProviderBaseURL(host) error = %v", err)
	}
	if got != "https://github.com" {
		t.Fatalf("NormalizeProviderBaseURL(host) = %q", got)
	}

	if _, err := NormalizeProviderBaseURL("ssh://github.com"); !errors.Is(err, ErrInvalidRequest) {
		t.Fatalf("expected invalid request for unsupported scheme, got %v", err)
	}
}

func TestDecodeAllowedUsagesDoesNotFailOpenForInvalidPersistedValues(t *testing.T) {
	tests := []struct {
		name string
		raw  string
		want []string
	}{
		{
			name: "valid KB mirror usage",
			raw:  `["kb_mirror"]`,
			want: []string{UsageKBMirror},
		},
		{
			name: "unsupported usage",
			raw:  `["other"]`,
			want: nil,
		},
		{
			name: "invalid json",
			raw:  `not-json`,
			want: nil,
		},
		{
			name: "empty array",
			raw:  `[]`,
			want: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := DecodeAllowedUsages(tt.raw)
			if len(got) != len(tt.want) {
				t.Fatalf("DecodeAllowedUsages() = %#v, want %#v", got, tt.want)
			}
			for i := range got {
				if got[i] != tt.want[i] {
					t.Fatalf("DecodeAllowedUsages() = %#v, want %#v", got, tt.want)
				}
			}
		})
	}
}
