package service

import (
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"math/big"
	"strings"
	"testing"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/google/go-cmp/cmp"
	"github.com/int128/kubelogin/integration_test/oidcserver/testconfig"
	testingJWT "github.com/int128/kubelogin/pkg/testing/jwt"
)

func New(t *testing.T, issuerURL string, config testconfig.Config) Service {
	return &service{
		config:    config,
		t:         t,
		issuerURL: issuerURL,
	}
}

type service struct {
	config                    testconfig.Config
	t                         *testing.T
	issuerURL                 string
	lastAuthenticationRequest *AuthenticationRequest
	lastTokenResponse         *TokenResponse
}

func (svc *service) IssuerURL() string {
	return svc.issuerURL
}

func (svc *service) SetConfig(cfg testconfig.Config) {
	svc.config = cfg
}

func (svc *service) LastTokenResponse() *TokenResponse {
	return svc.lastTokenResponse
}

func (svc *service) Discovery() *DiscoveryResponse {
	// based on https://accounts.google.com/.well-known/openid-configuration
	return &DiscoveryResponse{
		Issuer:                            svc.issuerURL,
		AuthorizationEndpoint:             svc.issuerURL + "/auth",
		TokenEndpoint:                     svc.issuerURL + "/token",
		JwksURI:                           svc.issuerURL + "/certs",
		UserinfoEndpoint:                  svc.issuerURL + "/userinfo",
		RevocationEndpoint:                svc.issuerURL + "/revoke",
		ResponseTypesSupported:            []string{"code id_token"},
		SubjectTypesSupported:             []string{"public"},
		IDTokenSigningAlgValuesSupported:  []string{"RS256"},
		ScopesSupported:                   []string{"openid", "email", "profile"},
		TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic"},
		CodeChallengeMethodsSupported:     svc.config.Response.CodeChallengeMethodsSupported,
		ClaimsSupported:                   []string{"aud", "email", "exp", "iat", "iss", "name", "sub"},
	}
}

func (svc *service) GetCertificates() *CertificatesResponse {
	idTokenKeyPair := testingJWT.PrivateKey
	return &CertificatesResponse{
		Keys: []*CertificatesResponseKey{
			{
				Kty: "RSA",
				Alg: "RS256",
				Use: "sig",
				Kid: "dummy",
				E:   base64.RawURLEncoding.EncodeToString(big.NewInt(int64(idTokenKeyPair.E)).Bytes()),
				N:   base64.RawURLEncoding.EncodeToString(idTokenKeyPair.N.Bytes()),
			},
		},
	}
}

func (svc *service) AuthenticateCode(req AuthenticationRequest) (code string, err error) {
	if req.Scope != svc.config.Want.Scope {
		svc.t.Errorf("scope wants `%s` but was `%s`", svc.config.Want.Scope, req.Scope)
	}
	if !strings.HasPrefix(req.RedirectURI, svc.config.Want.RedirectURIPrefix) {
		svc.t.Errorf("redirectURI wants prefix `%s` but was `%s`", svc.config.Want.RedirectURIPrefix, req.RedirectURI)
	}
	if diff := cmp.Diff(svc.config.Want.CodeChallengeMethod, req.CodeChallengeMethod); diff != "" {
		svc.t.Errorf("code_challenge_method mismatch (-want +got):\n%s", diff)
	}
	for k, v := range svc.config.Want.ExtraParams {
		got := req.RawQuery.Get(k)
		if got != v {
			svc.t.Errorf("parameter %s wants `%s` but was `%s`", k, v, got)
		}
	}
	svc.lastAuthenticationRequest = &req
	return "YOUR_AUTH_CODE", nil
}

func (svc *service) Exchange(req TokenRequest) (*TokenResponse, error) {
	if req.Code != "YOUR_AUTH_CODE" {
		return nil, fmt.Errorf("code wants %s but was %s", "YOUR_AUTH_CODE", req.Code)
	}
	if svc.lastAuthenticationRequest.CodeChallengeMethod == "S256" {
		// https://tools.ietf.org/html/rfc7636#section-4.6
		challenge := computeS256Challenge(req.CodeVerifier)
		if challenge != svc.lastAuthenticationRequest.CodeChallenge {
			svc.t.Errorf("pkce S256 challenge did not match (want %s but was %s)", svc.lastAuthenticationRequest.CodeChallenge, challenge)
		}
	}
	resp := &TokenResponse{
		TokenType:    "Bearer",
		ExpiresIn:    3600,
		AccessToken:  "YOUR_ACCESS_TOKEN",
		RefreshToken: svc.config.Response.RefreshToken,
		IDToken: testingJWT.EncodeF(svc.t, func(claims *testingJWT.Claims) {
			claims.Issuer = svc.issuerURL
			claims.Subject = "SUBJECT"
			claims.IssuedAt = jwt.NewNumericDate(svc.config.Response.IDTokenExpiry.Add(-time.Hour))
			claims.ExpiresAt = jwt.NewNumericDate(svc.config.Response.IDTokenExpiry)
			claims.Audience = []string{"kubernetes"}
			claims.Nonce = svc.lastAuthenticationRequest.Nonce
		}),
	}
	svc.lastTokenResponse = resp
	return resp, nil
}

func computeS256Challenge(verifier string) string {
	c := sha256.Sum256([]byte(verifier))
	return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(c[:])
}

func (svc *service) AuthenticatePassword(username, password, scope string) (*TokenResponse, error) {
	if scope != svc.config.Want.Scope {
		svc.t.Errorf("scope wants `%s` but was `%s`", svc.config.Want.Scope, scope)
	}
	if username != svc.config.Want.Username {
		svc.t.Errorf("username wants `%s` but was `%s`", svc.config.Want.Username, username)
	}
	if password != svc.config.Want.Password {
		svc.t.Errorf("password wants `%s` but was `%s`", svc.config.Want.Password, password)
	}
	resp := &TokenResponse{
		TokenType:    "Bearer",
		ExpiresIn:    3600,
		AccessToken:  "YOUR_ACCESS_TOKEN",
		RefreshToken: svc.config.Response.RefreshToken,
		IDToken: testingJWT.EncodeF(svc.t, func(claims *testingJWT.Claims) {
			claims.Issuer = svc.issuerURL
			claims.Subject = "SUBJECT"
			claims.IssuedAt = jwt.NewNumericDate(svc.config.Response.IDTokenExpiry.Add(-time.Hour))
			claims.ExpiresAt = jwt.NewNumericDate(svc.config.Response.IDTokenExpiry)
			claims.Audience = []string{"kubernetes"}
		}),
	}
	svc.lastTokenResponse = resp
	return resp, nil
}

func (svc *service) Refresh(refreshToken string) (*TokenResponse, error) {
	if refreshToken != svc.config.Want.RefreshToken {
		svc.t.Errorf("refreshToken wants %s but was %s", svc.config.Want.RefreshToken, refreshToken)
	}
	if svc.config.Response.RefreshError != "" {
		return nil, &ErrorResponse{Code: "invalid_request", Description: svc.config.Response.RefreshError}
	}
	resp := &TokenResponse{
		TokenType:    "Bearer",
		ExpiresIn:    3600,
		AccessToken:  "YOUR_ACCESS_TOKEN",
		RefreshToken: svc.config.Response.RefreshToken,
		IDToken: testingJWT.EncodeF(svc.t, func(claims *testingJWT.Claims) {
			claims.Issuer = svc.issuerURL
			claims.Subject = "SUBJECT"
			claims.IssuedAt = jwt.NewNumericDate(svc.config.Response.IDTokenExpiry.Add(-time.Hour))
			claims.ExpiresAt = jwt.NewNumericDate(svc.config.Response.IDTokenExpiry)
			claims.Audience = []string{"kubernetes"}
		}),
	}
	svc.lastTokenResponse = resp
	return resp, nil
}
