package query

import (
	"database/sql"
	"database/sql/driver"
	_ "embed"
	"encoding/json"
	"regexp"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"golang.org/x/text/language"

	"github.com/zitadel/zitadel/internal/api/authz"
	"github.com/zitadel/zitadel/internal/database"
	"github.com/zitadel/zitadel/internal/domain"
	"github.com/zitadel/zitadel/internal/zerrors"
)

var (
	//go:embed testdata/userinfo_not_found.json
	testdataUserInfoNotFound string
	//go:embed testdata/userinfo_human_no_md.json
	testdataUserInfoHumanNoMD string
	//go:embed testdata/userinfo_human.json
	testdataUserInfoHuman string
	//go:embed testdata/userinfo_human_grants.json
	testdataUserInfoHumanGrants string
	//go:embed testdata/userinfo_machine.json
	testdataUserInfoMachine string

	// timeLocation does a single parse of the testdata and extracts a time.Location,
	// so that it may be used during test assertion.
	// Because depending on the environment json.Unmarshal parses
	// the 00:00 timezones differently.
	// On my local system is parses to an empty timezone with 0 offset,
	// but in github action is pares into UTC.
	timeLocation = func() *time.Location {
		referenceInfo := new(OIDCUserInfo)
		err := json.Unmarshal([]byte(testdataUserInfoHumanNoMD), referenceInfo)
		if err != nil {
			panic(err)
		}
		return referenceInfo.User.CreationDate.Location()
	}()
)

func TestQueries_GetOIDCUserInfo(t *testing.T) {
	type args struct {
		userID       string
		roleAudience []string
		roleOrgIDs   []string
	}
	tests := []struct {
		name    string
		args    args
		mock    sqlExpectation
		want    *OIDCUserInfo
		wantErr error
	}{
		{
			name: "query error",
			args: args{
				userID: "231965491734773762",
			},
			mock:    mockQueryErr(regexp.QuoteMeta(oidcUserInfoQuery), sql.ErrConnDone, "231965491734773762", "instanceID", database.TextArray[string](nil)),
			wantErr: sql.ErrConnDone,
		},
		{
			name: "user not found",
			args: args{
				userID: "231965491734773762",
			},
			mock:    mockQuery(regexp.QuoteMeta(oidcUserInfoQuery), []string{"json_build_object"}, []driver.Value{testdataUserInfoNotFound}, "231965491734773762", "instanceID", database.TextArray[string](nil)),
			wantErr: zerrors.ThrowNotFound(nil, "QUERY-ahs4S", "Errors.User.NotFound"),
		},
		{
			name: "human without metadata",
			args: args{
				userID: "231965491734773762",
			},
			mock: mockQuery(regexp.QuoteMeta(oidcUserInfoQuery), []string{"json_build_object"}, []driver.Value{testdataUserInfoHumanNoMD}, "231965491734773762", "instanceID", database.TextArray[string](nil)),
			want: &OIDCUserInfo{
				User: &User{
					ID:                 "231965491734773762",
					CreationDate:       time.Date(2023, time.September, 15, 6, 10, 7, 434142000, timeLocation),
					ChangeDate:         time.Date(2023, time.November, 14, 13, 27, 2, 72318000, timeLocation),
					Sequence:           1148,
					State:              1,
					ResourceOwner:      "231848297847848962",
					Username:           "tim+tesmail@zitadel.com",
					PreferredLoginName: "tim+tesmail@zitadel.com@demo.localhost",
					Human: &Human{
						FirstName:         "Tim",
						LastName:          "Mohlmann",
						NickName:          "muhlemmer",
						DisplayName:       "Tim Mohlmann",
						AvatarKey:         "",
						PreferredLanguage: language.English,
						Gender:            domain.GenderMale,
						Email:             "tim+tesmail@zitadel.com",
						IsEmailVerified:   true,
						Phone:             "+40123456789",
						IsPhoneVerified:   false,
					},
					Machine: nil,
				},
				Org: &UserInfoOrg{
					ID:            "231848297847848962",
					Name:          "demo",
					PrimaryDomain: "demo.localhost",
				},
				Metadata: nil,
			},
		},
		{
			name: "human with metadata",
			args: args{
				userID: "231965491734773762",
			},
			mock: mockQuery(regexp.QuoteMeta(oidcUserInfoQuery), []string{"json_build_object"}, []driver.Value{testdataUserInfoHuman}, "231965491734773762", "instanceID", database.TextArray[string](nil)),
			want: &OIDCUserInfo{
				User: &User{
					ID:                 "231965491734773762",
					CreationDate:       time.Date(2023, time.September, 15, 6, 10, 7, 434142000, timeLocation),
					ChangeDate:         time.Date(2023, time.November, 14, 13, 27, 2, 72318000, timeLocation),
					Sequence:           1148,
					State:              1,
					ResourceOwner:      "231848297847848962",
					Username:           "tim+tesmail@zitadel.com",
					PreferredLoginName: "tim+tesmail@zitadel.com@demo.localhost",
					Human: &Human{
						FirstName:         "Tim",
						LastName:          "Mohlmann",
						NickName:          "muhlemmer",
						DisplayName:       "Tim Mohlmann",
						AvatarKey:         "",
						PreferredLanguage: language.English,
						Gender:            domain.GenderMale,
						Email:             "tim+tesmail@zitadel.com",
						IsEmailVerified:   true,
						Phone:             "+40123456789",
						IsPhoneVerified:   false,
					},
					Machine: nil,
				},
				Org: &UserInfoOrg{
					ID:            "231848297847848962",
					Name:          "demo",
					PrimaryDomain: "demo.localhost",
				},
				Metadata: []UserMetadata{
					{
						CreationDate:  time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
						ChangeDate:    time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
						Sequence:      1147,
						ResourceOwner: "231848297847848962",
						Key:           "bar",
						Value:         []byte("foo"),
					},
					{
						CreationDate:  time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
						ChangeDate:    time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
						Sequence:      1146,
						ResourceOwner: "231848297847848962",
						Key:           "foo",
						Value:         []byte("bar"),
					},
				},
			},
		},
		{
			name: "human with metadata and grants",
			args: args{
				userID:       "231965491734773762",
				roleAudience: []string{"236645808328409090", "240762134579904514"},
			},
			mock: mockQuery(regexp.QuoteMeta(oidcUserInfoQuery),
				[]string{"json_build_object"},
				[]driver.Value{testdataUserInfoHumanGrants},
				"231965491734773762", "instanceID", database.TextArray[string]{"236645808328409090", "240762134579904514"},
			),
			want: &OIDCUserInfo{
				User: &User{
					ID:                 "231965491734773762",
					CreationDate:       time.Date(2023, time.September, 15, 6, 10, 7, 434142000, timeLocation),
					ChangeDate:         time.Date(2023, time.November, 14, 13, 27, 2, 72318000, timeLocation),
					Sequence:           1148,
					State:              1,
					ResourceOwner:      "231848297847848962",
					Username:           "tim+tesmail@zitadel.com",
					PreferredLoginName: "tim+tesmail@zitadel.com@demo.localhost",
					Human: &Human{
						FirstName:         "Tim",
						LastName:          "Mohlmann",
						NickName:          "muhlemmer",
						DisplayName:       "Tim Mohlmann",
						AvatarKey:         "",
						PreferredLanguage: language.English,
						Gender:            domain.GenderMale,
						Email:             "tim+tesmail@zitadel.com",
						IsEmailVerified:   true,
						Phone:             "+40123456789",
						IsPhoneVerified:   false,
					},
					Machine: nil,
				},
				Org: &UserInfoOrg{
					ID:            "231848297847848962",
					Name:          "demo",
					PrimaryDomain: "demo.localhost",
				},
				Metadata: []UserMetadata{
					{
						CreationDate:  time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
						ChangeDate:    time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
						Sequence:      1147,
						ResourceOwner: "231848297847848962",
						Key:           "bar",
						Value:         []byte("foo"),
					},
					{
						CreationDate:  time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
						ChangeDate:    time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
						Sequence:      1146,
						ResourceOwner: "231848297847848962",
						Key:           "foo",
						Value:         []byte("bar"),
					},
				},
				UserGrants: []UserGrant{
					{
						ID:           "240749256523120642",
						GrantID:      "",
						State:        1,
						CreationDate: time.Date(2023, time.November, 14, 20, 28, 59, 168208000, timeLocation),
						ChangeDate:   time.Date(2023, time.November, 14, 20, 50, 58, 822391000, timeLocation),
						Sequence:     2,
						UserID:       "231965491734773762",
						Roles: []string{
							"role1",
							"role2",
						},
						ResourceOwner:     "231848297847848962",
						ProjectID:         "236645808328409090",
						OrgName:           "demo",
						OrgPrimaryDomain:  "demo.localhost",
						ProjectName:       "tests",
						UserResourceOwner: "231848297847848962",
					},
					{
						ID:           "240762315572510722",
						GrantID:      "",
						State:        1,
						CreationDate: time.Date(2023, time.November, 14, 22, 38, 42, 967317000, timeLocation),
						ChangeDate:   time.Date(2023, time.November, 14, 22, 38, 42, 967317000, timeLocation),
						Sequence:     1,
						UserID:       "231965491734773762",
						Roles: []string{
							"role3",
							"role4",
						},
						ResourceOwner:     "231848297847848962",
						ProjectID:         "240762134579904514",
						OrgName:           "demo",
						OrgPrimaryDomain:  "demo.localhost",
						ProjectName:       "tests2",
						UserResourceOwner: "231848297847848962",
					},
				},
			},
		},
		{
			name: "human with metadata and grants, role orgIDs",
			args: args{
				userID:       "231965491734773762",
				roleAudience: []string{"236645808328409090", "240762134579904514"},
				roleOrgIDs:   []string{"231848297847848962"},
			},
			mock: mockQuery(regexp.QuoteMeta(oidcUserInfoWithRoleOrgIDsQuery),
				[]string{"json_build_object"},
				[]driver.Value{testdataUserInfoHumanGrants},
				"231965491734773762", "instanceID",
				database.TextArray[string]{"236645808328409090", "240762134579904514"},
				database.TextArray[string]{"231848297847848962"},
			),
			want: &OIDCUserInfo{
				User: &User{
					ID:                 "231965491734773762",
					CreationDate:       time.Date(2023, time.September, 15, 6, 10, 7, 434142000, timeLocation),
					ChangeDate:         time.Date(2023, time.November, 14, 13, 27, 2, 72318000, timeLocation),
					Sequence:           1148,
					State:              1,
					ResourceOwner:      "231848297847848962",
					Username:           "tim+tesmail@zitadel.com",
					PreferredLoginName: "tim+tesmail@zitadel.com@demo.localhost",
					Human: &Human{
						FirstName:         "Tim",
						LastName:          "Mohlmann",
						NickName:          "muhlemmer",
						DisplayName:       "Tim Mohlmann",
						AvatarKey:         "",
						PreferredLanguage: language.English,
						Gender:            domain.GenderMale,
						Email:             "tim+tesmail@zitadel.com",
						IsEmailVerified:   true,
						Phone:             "+40123456789",
						IsPhoneVerified:   false,
					},
					Machine: nil,
				},
				Org: &UserInfoOrg{
					ID:            "231848297847848962",
					Name:          "demo",
					PrimaryDomain: "demo.localhost",
				},
				Metadata: []UserMetadata{
					{
						CreationDate:  time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
						ChangeDate:    time.Date(2023, time.November, 14, 13, 26, 3, 553702000, timeLocation),
						Sequence:      1147,
						ResourceOwner: "231848297847848962",
						Key:           "bar",
						Value:         []byte("foo"),
					},
					{
						CreationDate:  time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
						ChangeDate:    time.Date(2023, time.November, 14, 13, 25, 57, 171368000, timeLocation),
						Sequence:      1146,
						ResourceOwner: "231848297847848962",
						Key:           "foo",
						Value:         []byte("bar"),
					},
				},
				UserGrants: []UserGrant{
					{
						ID:           "240749256523120642",
						GrantID:      "",
						State:        1,
						CreationDate: time.Date(2023, time.November, 14, 20, 28, 59, 168208000, timeLocation),
						ChangeDate:   time.Date(2023, time.November, 14, 20, 50, 58, 822391000, timeLocation),
						Sequence:     2,
						UserID:       "231965491734773762",
						Roles: []string{
							"role1",
							"role2",
						},
						ResourceOwner:     "231848297847848962",
						ProjectID:         "236645808328409090",
						OrgName:           "demo",
						OrgPrimaryDomain:  "demo.localhost",
						ProjectName:       "tests",
						UserResourceOwner: "231848297847848962",
					},
					{
						ID:           "240762315572510722",
						GrantID:      "",
						State:        1,
						CreationDate: time.Date(2023, time.November, 14, 22, 38, 42, 967317000, timeLocation),
						ChangeDate:   time.Date(2023, time.November, 14, 22, 38, 42, 967317000, timeLocation),
						Sequence:     1,
						UserID:       "231965491734773762",
						Roles: []string{
							"role3",
							"role4",
						},
						ResourceOwner:     "231848297847848962",
						ProjectID:         "240762134579904514",
						OrgName:           "demo",
						OrgPrimaryDomain:  "demo.localhost",
						ProjectName:       "tests2",
						UserResourceOwner: "231848297847848962",
					},
				},
			},
		},
		{
			name: "machine with metadata",
			args: args{
				userID: "240707570677841922",
			},
			mock: mockQuery(regexp.QuoteMeta(oidcUserInfoQuery), []string{"json_build_object"}, []driver.Value{testdataUserInfoMachine}, "240707570677841922", "instanceID", database.TextArray[string](nil)),
			want: &OIDCUserInfo{
				User: &User{
					ID:                 "240707570677841922",
					CreationDate:       time.Date(2023, time.November, 14, 13, 34, 52, 473732000, timeLocation),
					ChangeDate:         time.Date(2023, time.November, 14, 13, 35, 2, 861342000, timeLocation),
					Sequence:           2,
					State:              1,
					ResourceOwner:      "231848297847848962",
					Username:           "tests",
					PreferredLoginName: "tests@demo.localhost",
					Human:              nil,
					Machine: &Machine{
						Name:        "tests",
						Description: "My test service user",
					},
				},
				Org: &UserInfoOrg{
					ID:            "231848297847848962",
					Name:          "demo",
					PrimaryDomain: "demo.localhost",
				},
				Metadata: []UserMetadata{
					{
						CreationDate:  time.Date(2023, time.November, 14, 13, 35, 30, 126849000, timeLocation),
						ChangeDate:    time.Date(2023, time.November, 14, 13, 35, 30, 126849000, timeLocation),
						Sequence:      3,
						ResourceOwner: "231848297847848962",
						Key:           "first",
						Value:         []byte("Hello World!"),
					},
					{
						CreationDate:  time.Date(2023, time.November, 14, 13, 35, 44, 28343000, timeLocation),
						ChangeDate:    time.Date(2023, time.November, 14, 13, 35, 44, 28343000, timeLocation),
						Sequence:      4,
						ResourceOwner: "231848297847848962",
						Key:           "second",
						Value:         []byte("Bye World!"),
					},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			execMock(t, tt.mock, func(db *sql.DB) {
				q := &Queries{
					client: &database.DB{
						DB:       db,
						Database: &prepareDB{},
					},
				}
				ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")

				got, err := q.GetOIDCUserInfo(ctx, tt.args.userID, tt.args.roleAudience, tt.args.roleOrgIDs...)
				require.ErrorIs(t, err, tt.wantErr)
				assert.Equal(t, tt.want, got)
			})
		})
	}
}

func TestQueries_GetOIDCUserinfoClientByID(t *testing.T) {
	expQuery := regexp.QuoteMeta(oidcUserinfoClientQuery)
	cols := []string{"project_id", "project_role_assertion"}

	tests := []struct {
		name                     string
		mock                     sqlExpectation
		wantProjectID            string
		wantProjectRoleAssertion bool
		wantErr                  error
	}{
		{
			name:    "no rows",
			mock:    mockQueryErr(expQuery, sql.ErrNoRows, "instanceID", "clientID"),
			wantErr: zerrors.ThrowNotFound(sql.ErrNoRows, "QUERY-beeW8", "Errors.App.NotFound"),
		},
		{
			name:    "internal error",
			mock:    mockQueryErr(expQuery, sql.ErrConnDone, "instanceID", "clientID"),
			wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ais4r", "Errors.Internal"),
		},
		{
			name:                     "found",
			mock:                     mockQuery(expQuery, cols, []driver.Value{"projectID", true}, "instanceID", "clientID"),
			wantProjectID:            "projectID",
			wantProjectRoleAssertion: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			execMock(t, tt.mock, func(db *sql.DB) {
				q := &Queries{
					client: &database.DB{
						DB:       db,
						Database: &prepareDB{},
					},
				}
				ctx := authz.NewMockContext("instanceID", "orgID", "loginClient")
				gotProjectID, gotProjectRoleAssertion, err := q.GetOIDCUserinfoClientByID(ctx, "clientID")
				require.ErrorIs(t, err, tt.wantErr)
				assert.Equal(t, tt.wantProjectID, gotProjectID)
				assert.Equal(t, tt.wantProjectRoleAssertion, gotProjectRoleAssertion)
			})
		})
	}
}