package colabauth

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"testing"

	"github.com/stretchr/testify/require"
)

func poolCredential(subject string) credentials {
	return credentials{
		Type: "authorized_user", ClientID: "client", ClientSecret: "secret",
		RefreshToken: "refresh-" + subject, GoogleSubject: subject, Email: subject + "@example.com",
	}
}

func TestMultipleAccountBrowserFlow(t *testing.T) {
	e := newEnv(t, "")
	c := e.browser(owner)
	signIn := func(profile, refresh string) {
		e.profile, e.refresh = profile, refresh
		state := e.start(c).Query().Get("state")
		require.Equal(t, http.StatusFound, e.callback(c, "code=code&state="+state).StatusCode)
	}
	signIn(`{"sub":"first","email":"first@example.com","email_verified":true}`, "refresh-first")
	store, err := e.h.readAccounts()
	require.NoError(t, err)
	first := store.Accounts[0].AccountID
	signIn(`{"sub":"second","email":"second@example.com","email_verified":true}`, "refresh-second")
	signIn(`{"sub":"first","email":"renamed@example.com","email_verified":true}`, "refresh-new")
	store, err = e.h.readAccounts()
	require.NoError(t, err)
	require.Len(t, store.Accounts, 2)
	require.Equal(t, first, store.Accounts[0].AccountID)
	require.Equal(t, "refresh-new", store.Accounts[0].RefreshToken)
	require.Equal(t, "refresh-second", store.Accounts[1].RefreshToken)
	require.NotEqual(t, first, store.Accounts[1].AccountID)
	page, err := c.Get(e.srv.URL + PageEndpoint)
	require.NoError(t, err)
	html := body(t, page)
	require.Contains(t, html, "renamed@example.com")
	require.Contains(t, html, "second@example.com")
	for _, secret := range []string{"refresh-new", "refresh-second", "csecret", `value="at"`} {
		require.NotContains(t, html, secret)
	}
	// a failed refresh/connection leaves both accounts untouched.
	before, err := os.ReadFile(e.h.accountPath())
	require.NoError(t, err)
	e.refresh = ""
	state := e.start(c).Query().Get("state")
	require.Equal(t, http.StatusBadGateway, e.callback(c, "code=code&state="+state).StatusCode)
	after, err := os.ReadFile(e.h.accountPath())
	require.NoError(t, err)
	require.Equal(t, before, after)
}

func TestLegacyImportKeepsCredentialAndStableID(t *testing.T) {
	e := newEnv(t, "")
	legacy := poolCredential("legacy")
	legacy.GoogleSubject = ""
	require.NoError(t, writePrivateJSON(e.path, legacy))
	original, err := os.ReadFile(e.path)
	require.NoError(t, err)
	require.NoError(t, e.h.initializeAccounts())
	store, err := e.h.readAccounts()
	require.NoError(t, err)
	require.Len(t, store.Accounts, 1)
	id := store.Accounts[0].AccountID
	require.Regexp(t, accountIDPattern, id)
	require.NoError(t, e.h.initializeAccounts())
	upgraded := poolCredential("google-subject")
	upgraded.Email = strings.ToUpper(legacy.Email)
	require.NoError(t, e.h.save(upgraded))
	store, err = e.h.readAccounts()
	require.NoError(t, err)
	require.Len(t, store.Accounts, 1)
	require.Equal(t, id, store.Accounts[0].AccountID)
	require.Equal(t, upgraded.GoogleSubject, store.Accounts[0].GoogleSubject)
	after, err := os.ReadFile(e.path)
	require.NoError(t, err)
	require.Equal(t, original, after)
	removed, err := e.h.removeAccount(id)
	require.NoError(t, err)
	require.True(t, removed)
	require.NoError(t, e.h.initializeAccounts())
	store, err = e.h.readAccounts()
	require.NoError(t, err)
	require.Empty(t, store.Accounts, "empty pool must not resurrect rollback credential")
	require.NoError(t, e.h.save(upgraded))
	store, err = e.h.readAccounts()
	require.NoError(t, err)
	require.NotEqual(t, id, store.Accounts[0].AccountID, "removal invalidates old handles permanently")
}

func TestConcurrentAccountsAndPoolCap(t *testing.T) {
	e := newEnv(t, "")
	var wg sync.WaitGroup
	errs := make(chan error, maxAccounts)
	for i := 0; i < maxAccounts; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			errs <- e.h.save(poolCredential(fmt.Sprint(i)))
		}(i)
	}
	wg.Wait()
	close(errs)
	for err := range errs {
		require.NoError(t, err)
	}
	store, err := e.h.readAccounts()
	require.NoError(t, err)
	require.Len(t, store.Accounts, maxAccounts)
	before, err := os.ReadFile(e.h.accountPath())
	require.NoError(t, err)
	require.ErrorIs(t, e.h.save(poolCredential("overflow")), errPoolFull)
	after, err := os.ReadFile(e.h.accountPath())
	require.NoError(t, err)
	require.Equal(t, before, after)
	updated := poolCredential("0")
	updated.RefreshToken = "new-refresh"
	require.NoError(t, e.h.save(updated), "reconnect remains possible at the cap")
	info, err := os.Stat(e.h.accountPath())
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
}

func TestRemoveRequiresOwnerCSRFAndOrigin(t *testing.T) {
	e := newEnv(t, "")
	require.NoError(t, e.h.save(poolCredential("one")))
	store, err := e.h.readAccounts()
	require.NoError(t, err)
	id := store.Accounts[0].AccountID
	c := e.browser(owner)
	csrf := e.csrf(c)
	post := func(client *http.Client, token, origin string) int {
		req, _ := http.NewRequest(http.MethodPost, e.srv.URL+PageEndpoint+"/remove", strings.NewReader(url.Values{"csrf": {token}, "account_id": {id}}.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.Header.Set("Origin", origin)
		res, err := client.Do(req)
		require.NoError(t, err)
		res.Body.Close()
		return res.StatusCode
	}
	require.Equal(t, http.StatusForbidden, post(e.browser("other"), csrf, "https://gw.example"))
	require.Equal(t, http.StatusForbidden, post(c, "wrong", "https://gw.example"))
	require.Equal(t, http.StatusForbidden, post(c, csrf, "null"))
	require.Equal(t, http.StatusForbidden, post(c, csrf, "https://evil.example"))
	require.Equal(t, http.StatusSeeOther, post(c, csrf, "https://gw.example"))
	require.Equal(t, http.StatusNotFound, post(c, csrf, "https://gw.example"))
	store, err = e.h.readAccounts()
	require.NoError(t, err)
	require.Empty(t, store.Accounts)
}

func TestStoreFailuresDoNotOverwriteAccounts(t *testing.T) {
	e := newEnv(t, "")
	require.NoError(t, e.h.save(poolCredential("one")))
	before, err := os.ReadFile(e.h.accountPath())
	require.NoError(t, err)
	require.NoError(t, os.Chmod(e.h.accountPath(), 0o644))
	require.Error(t, e.h.save(poolCredential("two")))
	after, err := os.ReadFile(e.h.accountPath())
	require.NoError(t, err)
	require.Equal(t, before, after)
	require.NoError(t, os.Chmod(e.h.accountPath(), 0o600))
	for _, invalid := range []string{`{}`, `{"version":2,"accounts":[]}`, `{"version":1,"accounts":null}`, strings.Repeat("x", maxAccountBytes+1)} {
		require.NoError(t, os.WriteFile(e.h.accountPath(), []byte(invalid), 0o600))
		require.Error(t, e.h.initializeAccounts())
		require.Error(t, e.h.save(poolCredential("two")))
		after, err := os.ReadFile(e.h.accountPath())
		require.NoError(t, err)
		require.Equal(t, invalid, string(after))
	}
}

func TestStoreRejectsDuplicateIDs(t *testing.T) {
	e := newEnv(t, "")
	require.NoError(t, e.h.save(poolCredential("one")))
	store, err := e.h.readAccounts()
	require.NoError(t, err)
	store.Accounts = append(store.Accounts, store.Accounts[0])
	data, err := json.Marshal(store)
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(e.h.accountPath(), data, 0o600))
	_, err = e.h.readAccounts()
	require.Error(t, err)
}
