package core_test

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/tests"
	"github.com/pocketbase/pocketbase/tools/mailer"
	"github.com/pocketbase/pocketbase/tools/security"
)

func TestSettingsDelete(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	err := app.Delete(app.Settings())
	if err == nil {
		t.Fatal("Exected settings delete to fail")
	}
}

func TestSettings_DBExport(t *testing.T) {
	scenarios := []struct {
		name       string
		encryption bool
	}{
		{"no encryption", false},
		{"with encryption", true},
	}

	encryptionKey := strings.Repeat("a", 32)

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			app, _ := tests.NewTestApp()
			defer app.Cleanup()

			originalEnv := os.Getenv(app.EncryptionEnv())
			defer func() {
				os.Setenv(app.EncryptionEnv(), originalEnv)
			}()

			settings := &core.Settings{}
			settings.Meta.AppName = "test_app_name"
			settings.Logs.MaxDays = 123
			settings.SMTP.Host = "smtp_host"
			settings.SMTP.Username = "smtp_username"
			settings.SMTP.Password = "" // ensures that empty password is exported
			settings.S3.Endpoint = "s3_endpoint"
			settings.S3.Secret = "s3_secret"
			settings.Backups.Cron = "* * * * *"
			settings.Backups.S3.Enabled = true
			settings.Backups.S3.Secret = ""
			settings.Batch.Timeout = 15
			settings.RateLimits.Enabled = true
			settings.TrustedProxy.UseLeftmostIP = true

			if s.encryption {
				os.Setenv(app.EncryptionEnv(), encryptionKey)
			}

			export, err := settings.DBExport(app)
			if err != nil {
				t.Fatal(err)
			}

			var valueStr string

			if s.encryption {
				decrypted, err := security.Decrypt(export["value"].(string), encryptionKey)
				if err != nil {
					t.Fatalf("failed to decrypt test value: %v", err)
				}

				valueStr = string(decrypted)
			} else {
				valueStr = string(export["value"].([]byte))
			}

			expected := `{"superuserIPs":[],"smtp":{"enabled":false,"port":0,"host":"smtp_host","username":"smtp_username","password":"","authMethod":"","tls":false,"localName":""},"backups":{"cron":"* * * * *","cronMaxKeep":0,"s3":{"enabled":true,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false}},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"s3_endpoint","accessKey":"","secret":"s3_secret","forcePathStyle":false},"meta":{"accentColor":"","appName":"test_app_name","appURL":"","senderName":"","senderAddress":"","hideControls":false},"rateLimits":{"rules":[],"excludedIPs":[],"enabled":true},"trustedProxy":{"headers":[],"useLeftmostIP":true},"batch":{"enabled":false,"maxRequests":0,"timeout":15,"maxBodySize":0},"logs":{"maxDays":123,"minLevel":0,"logIP":false,"logAuthId":false}}`
			if valueStr != expected {
				t.Fatalf("Expected exported settings\n%s\ngot\n%s", expected, valueStr)
			}
		})
	}
}

func TestSettingsMerge(t *testing.T) {
	t.Parallel()

	s1 := &core.Settings{}
	s1.Meta.AppURL = "app_url" // should be unset

	s2 := &core.Settings{}
	s2.Meta.AppName = "test"
	s2.Logs.MaxDays = 123
	s2.SMTP.Host = "test"
	s2.SMTP.Enabled = true
	s2.S3.Enabled = true
	s2.S3.Endpoint = "test"
	s2.Backups.Cron = "* * * * *"
	s2.Batch.Timeout = 15

	if err := s1.Merge(s2); err != nil {
		t.Fatal(err)
	}

	s1Encoded, err := json.Marshal(s1)
	if err != nil {
		t.Fatal(err)
	}

	s2Encoded, err := json.Marshal(s2)
	if err != nil {
		t.Fatal(err)
	}

	if string(s1Encoded) != string(s2Encoded) {
		t.Fatalf("Expected the same serialization, got\n%v\nVS\n%v", string(s1Encoded), string(s2Encoded))
	}
}

func TestSettingsClone(t *testing.T) {
	t.Parallel()

	s1 := &core.Settings{}
	s1.Meta.AppName = "test_name"

	s2, err := s1.Clone()
	if err != nil {
		t.Fatal(err)
	}

	s1Bytes, err := json.Marshal(s1)
	if err != nil {
		t.Fatal(err)
	}

	s2Bytes, err := json.Marshal(s2)
	if err != nil {
		t.Fatal(err)
	}

	if string(s1Bytes) != string(s2Bytes) {
		t.Fatalf("Expected equivalent serialization, got %v VS %v", string(s1Bytes), string(s2Bytes))
	}

	// verify that it is a deep copy
	s2.Meta.AppName = "new_test_name"
	if s1.Meta.AppName == s2.Meta.AppName {
		t.Fatalf("Expected s1 and s2 to have different Meta.AppName, got %s", s1.Meta.AppName)
	}
}

func TestSettingsMarshalJSON(t *testing.T) {
	t.Parallel()

	settings := &core.Settings{}

	// control fields
	settings.Meta.AppName = "test123"
	settings.SMTP.Username = "abc"

	// secrets
	testSecret := "test_secret"
	settings.SMTP.Password = testSecret
	settings.S3.Secret = testSecret
	settings.Backups.S3.Secret = testSecret

	raw, err := json.Marshal(settings)
	if err != nil {
		t.Fatal(err)
	}
	rawStr := string(raw)

	expected := `{"superuserIPs":[],"smtp":{"enabled":false,"port":0,"host":"","username":"abc","authMethod":"","tls":false,"localName":""},"backups":{"cron":"","cronMaxKeep":0,"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false}},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false},"meta":{"accentColor":"","appName":"test123","appURL":"","senderName":"","senderAddress":"","hideControls":false},"rateLimits":{"rules":[],"excludedIPs":[],"enabled":false},"trustedProxy":{"headers":[],"useLeftmostIP":false},"batch":{"enabled":false,"maxRequests":0,"timeout":0,"maxBodySize":0},"logs":{"maxDays":0,"minLevel":0,"logIP":false,"logAuthId":false}}`

	if rawStr != expected {
		t.Fatalf("Expected\n%v\ngot\n%v", expected, rawStr)
	}
}

func TestSettingsValidate(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	s := app.Settings()

	// set invalid settings data
	s.SuperuserIPs = []string{"127.0.0.1", ""}
	s.Meta.AppName = ""
	s.Logs.MaxDays = -10
	s.SMTP.Enabled = true
	s.SMTP.Host = ""
	s.S3.Enabled = true
	s.S3.Endpoint = "invalid"
	s.Backups.Cron = "invalid"
	s.Backups.CronMaxKeep = -10
	s.Batch.Enabled = true
	s.Batch.MaxRequests = -1
	s.Batch.Timeout = -1
	s.RateLimits.Enabled = true
	s.RateLimits.Rules = nil

	// check if Validate() is triggering the members validate methods.
	err := app.Validate(s)
	if err == nil {
		t.Fatalf("Expected error, got nil")
	}

	expectations := []string{
		`"superuserIPs":{`,
		`"meta":{`,
		`"logs":{`,
		`"smtp":{`,
		`"s3":{`,
		`"backups":{`,
		`"batch":{`,
		`"rateLimits":{`,
	}

	errBytes, _ := json.Marshal(err)
	jsonErr := string(errBytes)
	for _, expected := range expectations {
		if !strings.Contains(jsonErr, expected) {
			t.Errorf("Expected error key %s in %v", expected, jsonErr)
		}
	}
}

func TestMetaConfigValidate(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		name           string
		config         core.MetaConfig
		expectedErrors []string
	}{
		{
			"zero values",
			core.MetaConfig{},
			[]string{
				"appName",
				"appURL",
				"senderName",
				"senderAddress",
			},
		},
		{
			"invalid data",
			core.MetaConfig{
				AccentColor:   "#fff",
				AppName:       strings.Repeat("a", 300),
				AppURL:        "test",
				SenderName:    strings.Repeat("a", 300),
				SenderAddress: "invalid_email",
			},
			[]string{
				"accentColor",
				"appName",
				"appURL",
				"senderName",
				"senderAddress",
			},
		},
		{
			"valid data",
			core.MetaConfig{
				AccentColor:   "#ffffff",
				AppName:       "test",
				AppURL:        "https://example.com",
				SenderName:    "test",
				SenderAddress: "test@example.com",
			},
			[]string{},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.config.Validate()

			tests.TestValidationErrors(t, result, s.expectedErrors)
		})
	}
}

func TestLogsConfigValidate(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		name           string
		config         core.LogsConfig
		expectedErrors []string
	}{
		{
			"zero values",
			core.LogsConfig{},
			[]string{},
		},
		{
			"invalid data",
			core.LogsConfig{MaxDays: -1},
			[]string{"maxDays"},
		},
		{
			"valid data",
			core.LogsConfig{MaxDays: 2},
			[]string{},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.config.Validate()

			tests.TestValidationErrors(t, result, s.expectedErrors)
		})
	}
}

func TestSMTPConfigValidate(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		name           string
		config         core.SMTPConfig
		expectedErrors []string
	}{
		{
			"zero values (disabled)",
			core.SMTPConfig{},
			[]string{},
		},
		{
			"zero values (enabled)",
			core.SMTPConfig{Enabled: true},
			[]string{"host", "port"},
		},
		{
			"invalid data",
			core.SMTPConfig{
				Enabled:    true,
				Host:       "test:test:test",
				Port:       -10,
				LocalName:  "invalid!",
				AuthMethod: "invalid",
			},
			[]string{"host", "port", "authMethod", "localName"},
		},
		{
			"valid data (no explicit auth method and localName)",
			core.SMTPConfig{
				Enabled: true,
				Host:    "example.com",
				Port:    100,
				TLS:     true,
			},
			[]string{},
		},
		{
			"valid data (explicit auth method and localName)",
			core.SMTPConfig{
				Enabled:    true,
				Host:       "example.com",
				Port:       100,
				AuthMethod: mailer.SMTPAuthLogin,
				LocalName:  "example.com",
			},
			[]string{},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.config.Validate()

			tests.TestValidationErrors(t, result, s.expectedErrors)
		})
	}
}

func TestS3ConfigValidate(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		name           string
		config         core.S3Config
		expectedErrors []string
	}{
		{
			"zero values (disabled)",
			core.S3Config{},
			[]string{},
		},
		{
			"zero values (enabled)",
			core.S3Config{Enabled: true},
			[]string{
				"bucket",
				"region",
				"endpoint",
				"accessKey",
				"secret",
			},
		},
		{
			"invalid data",
			core.S3Config{
				Enabled:  true,
				Endpoint: "test:test:test",
			},
			[]string{
				"bucket",
				"region",
				"endpoint",
				"accessKey",
				"secret",
			},
		},
		{
			"valid data (url endpoint)",
			core.S3Config{
				Enabled:   true,
				Endpoint:  "https://localhost:8090",
				Bucket:    "test",
				Region:    "test",
				AccessKey: "test",
				Secret:    "test",
			},
			[]string{},
		},
		{
			"valid data (hostname endpoint)",
			core.S3Config{
				Enabled:   true,
				Endpoint:  "example.com",
				Bucket:    "test",
				Region:    "test",
				AccessKey: "test",
				Secret:    "test",
			},
			[]string{},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.config.Validate()

			tests.TestValidationErrors(t, result, s.expectedErrors)
		})
	}
}

func TestBackupsConfigValidate(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		name           string
		config         core.BackupsConfig
		expectedErrors []string
	}{
		{
			"zero value",
			core.BackupsConfig{},
			[]string{},
		},
		{
			"invalid cron",
			core.BackupsConfig{
				Cron:        "invalid",
				CronMaxKeep: 0,
			},
			[]string{"cron", "cronMaxKeep"},
		},
		{
			"invalid enabled S3",
			core.BackupsConfig{
				S3: core.S3Config{
					Enabled: true,
				},
			},
			[]string{"s3"},
		},
		{
			"valid data",
			core.BackupsConfig{
				S3: core.S3Config{
					Enabled:   true,
					Endpoint:  "example.com",
					Bucket:    "test",
					Region:    "test",
					AccessKey: "test",
					Secret:    "test",
				},
				Cron:        "*/10 * * * *",
				CronMaxKeep: 1,
			},
			[]string{},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.config.Validate()

			tests.TestValidationErrors(t, result, s.expectedErrors)
		})
	}
}

func TestBatchConfigValidate(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		name           string
		config         core.BatchConfig
		expectedErrors []string
	}{
		{
			"zero value",
			core.BatchConfig{},
			[]string{},
		},
		{
			"zero value (enabled)",
			core.BatchConfig{Enabled: true},
			[]string{"maxRequests", "timeout"},
		},
		{
			"invalid data (negative values)",
			core.BatchConfig{
				MaxRequests: -1,
				Timeout:     -1,
				MaxBodySize: -1,
			},
			[]string{"maxRequests", "timeout", "maxBodySize"},
		},
		{
			"min fields valid data",
			core.BatchConfig{
				Enabled:     true,
				MaxRequests: 1,
				Timeout:     1,
			},
			[]string{},
		},
		{
			"all fields valid data",
			core.BatchConfig{
				Enabled:     true,
				MaxRequests: 10,
				Timeout:     1,
				MaxBodySize: 1,
			},
			[]string{},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.config.Validate()

			tests.TestValidationErrors(t, result, s.expectedErrors)
		})
	}
}

func TestRateLimitsConfigValidate(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		name           string
		config         core.RateLimitsConfig
		expectedErrors []string
	}{
		{
			"zero value (disabled)",
			core.RateLimitsConfig{},
			[]string{},
		},
		{
			"zero value (enabled)",
			core.RateLimitsConfig{Enabled: true},
			[]string{"rules"},
		},
		{
			"invalid data",
			core.RateLimitsConfig{
				Enabled:     true,
				ExcludedIPs: []string{"", "127.0.0.1"},
				Rules: []core.RateLimitRule{
					{
						Label:       "/123abc/",
						Duration:    1,
						MaxRequests: 2,
					},
					{
						Label:       "!abc",
						Duration:    -1,
						MaxRequests: -1,
					},
				},
			},
			[]string{"rules", "excludedIPs"},
		},
		{
			"valid data",
			core.RateLimitsConfig{
				Enabled:     true,
				ExcludedIPs: []string{"127.0.0.1", "10.0.0.1/20"},
				Rules: []core.RateLimitRule{
					{
						Label:       "123_abc",
						Duration:    1,
						MaxRequests: 2,
					},
					{
						Label:       "/456-abc",
						Duration:    1,
						MaxRequests: 2,
					},
				},
			},
			[]string{},
		},
		{
			"duplicated rules with the same audience",
			core.RateLimitsConfig{
				Enabled: true,
				Rules: []core.RateLimitRule{
					{
						Label:       "/a",
						Duration:    1,
						MaxRequests: 2,
					},
					{
						Label:       "/a",
						Duration:    2,
						MaxRequests: 3,
					},
				},
			},
			[]string{"rules"},
		},
		{
			"duplicated rule with conflicting audience (A)",
			core.RateLimitsConfig{
				Enabled: true,
				Rules: []core.RateLimitRule{
					{
						Label:       "/a",
						Duration:    1,
						MaxRequests: 2,
					},
					{
						Label:       "/a",
						Duration:    1,
						MaxRequests: 2,
						Audience:    core.RateLimitRuleAudienceGuest,
					},
				},
			},
			[]string{"rules"},
		},
		{
			"duplicated rule with conflicting audience (B)",
			core.RateLimitsConfig{
				Enabled: true,
				Rules: []core.RateLimitRule{
					{
						Label:       "/a",
						Duration:    1,
						MaxRequests: 2,
						Audience:    core.RateLimitRuleAudienceAuth,
					},
					{
						Label:       "/a",
						Duration:    1,
						MaxRequests: 2,
					},
				},
			},
			[]string{"rules"},
		},
		{
			"duplicated rule with non-conflicting audience",
			core.RateLimitsConfig{
				Enabled: true,
				Rules: []core.RateLimitRule{
					{
						Label:       "/a",
						Duration:    1,
						MaxRequests: 2,
						Audience:    core.RateLimitRuleAudienceAuth,
					},
					{
						Label:       "/a",
						Duration:    1,
						MaxRequests: 2,
						Audience:    core.RateLimitRuleAudienceGuest,
					},
				},
			},
			[]string{},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.config.Validate()

			tests.TestValidationErrors(t, result, s.expectedErrors)
		})
	}
}

func TestRateLimitsFindRateLimitRule(t *testing.T) {
	t.Parallel()

	limits := core.RateLimitsConfig{
		Rules: []core.RateLimitRule{
			{Label: "abc"},
			{Label: "def", Audience: core.RateLimitRuleAudienceGuest},
			{Label: "/test/a", Audience: core.RateLimitRuleAudienceGuest},
			{Label: "POST /test/a"},
			{Label: "/test/a/", Audience: core.RateLimitRuleAudienceAuth},
			{Label: "POST /test/a/"},
		},
	}

	scenarios := []struct {
		labels   []string
		audience []string
		expected string
	}{
		{[]string{}, []string{}, ""},
		{[]string{"missing"}, []string{}, ""},
		{[]string{"abc"}, []string{}, "abc"},
		{[]string{"abc"}, []string{core.RateLimitRuleAudienceGuest}, ""},
		{[]string{"abc"}, []string{core.RateLimitRuleAudienceAuth}, ""},
		{[]string{"def"}, []string{core.RateLimitRuleAudienceGuest}, "def"},
		{[]string{"def"}, []string{core.RateLimitRuleAudienceAuth}, ""},
		{[]string{"/test"}, []string{}, ""},
		{[]string{"/test/a"}, []string{}, "/test/a"},
		{[]string{"/test/a"}, []string{core.RateLimitRuleAudienceAuth}, "/test/a/"},
		{[]string{"/test/a"}, []string{core.RateLimitRuleAudienceGuest}, "/test/a"},
		{[]string{"GET /test/a"}, []string{}, ""},
		{[]string{"POST /test/a"}, []string{}, "POST /test/a"},
		{[]string{"/test/a/b/c"}, []string{}, "/test/a/"},
		{[]string{"/test/a/b/c"}, []string{core.RateLimitRuleAudienceAuth}, "/test/a/"},
		{[]string{"/test/a/b/c"}, []string{core.RateLimitRuleAudienceGuest}, ""},
		{[]string{"GET /test/a/b/c"}, []string{}, ""},
		{[]string{"POST /test/a/b/c"}, []string{}, "POST /test/a/"},
		{[]string{"/test/a", "abc"}, []string{}, "/test/a"}, // priority checks
	}

	for _, s := range scenarios {
		t.Run(strings.Join(s.labels, "_")+":"+strings.Join(s.audience, "_"), func(t *testing.T) {
			rule, ok := limits.FindRateLimitRule(s.labels, s.audience...)

			hasLabel := rule.Label != ""
			if hasLabel != ok {
				t.Fatalf("Expected hasLabel %v, got %v", hasLabel, ok)
			}

			if rule.Label != s.expected {
				t.Fatalf("Expected rule with label %q, got %q", s.expected, rule.Label)
			}
		})
	}
}

func TestRateLimitRuleValidate(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		name           string
		rule           core.RateLimitRule
		expectedErrors []string
	}{
		{
			"zero value",
			core.RateLimitRule{},
			[]string{"label", "duration", "maxRequests"},
		},
		{
			"invalid data",
			core.RateLimitRule{
				Label:       "@abc",
				Duration:    -1,
				MaxRequests: -1,
				Audience:    "invalid",
			},
			[]string{"label", "duration", "maxRequests", "audience"},
		},
		{
			"valid data (name)",
			core.RateLimitRule{
				Label:       "abc:123",
				Duration:    1,
				MaxRequests: 1,
			},
			[]string{},
		},
		{
			"valid data (name:action)",
			core.RateLimitRule{
				Label:       "abc:123",
				Duration:    1,
				MaxRequests: 1,
			},
			[]string{},
		},
		{
			"valid data (*:action)",
			core.RateLimitRule{
				Label:       "*:123",
				Duration:    1,
				MaxRequests: 1,
			},
			[]string{},
		},
		{
			"valid data (path /a/b)",
			core.RateLimitRule{
				Label:       "/a/b",
				Duration:    1,
				MaxRequests: 1,
			},
			[]string{},
		},
		{
			"valid data (path POST /a/b)",
			core.RateLimitRule{
				Label:       "POST /a/b/",
				Duration:    1,
				MaxRequests: 1,
			},
			[]string{},
		},
		{
			"invalid audience",
			core.RateLimitRule{
				Label:       "/a/b/",
				Duration:    1,
				MaxRequests: 1,
				Audience:    "invalid",
			},
			[]string{"audience"},
		},
		{
			"valid audience - " + core.RateLimitRuleAudienceGuest,
			core.RateLimitRule{
				Label:       "POST /a/b/",
				Duration:    1,
				MaxRequests: 1,
				Audience:    core.RateLimitRuleAudienceGuest,
			},
			[]string{},
		},
		{
			"valid audience - " + core.RateLimitRuleAudienceAuth,
			core.RateLimitRule{
				Label:       "POST /a/b/",
				Duration:    1,
				MaxRequests: 1,
				Audience:    core.RateLimitRuleAudienceAuth,
			},
			[]string{},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.rule.Validate()

			tests.TestValidationErrors(t, result, s.expectedErrors)
		})
	}
}

func TestRateLimitRuleDurationTime(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		rule     core.RateLimitRule
		expected time.Duration
	}{
		{core.RateLimitRule{}, 0 * time.Second},
		{core.RateLimitRule{Duration: 1234}, 1234 * time.Second},
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%d", i, s.rule.Duration), func(t *testing.T) {
			result := s.rule.DurationTime()

			if result != s.expected {
				t.Fatalf("Expected duration %d, got %d", s.expected, result)
			}
		})
	}
}

func TestRateLimitRuleString(t *testing.T) {
	t.Parallel()

	scenarios := []struct {
		name     string
		rule     core.RateLimitRule
		expected string
	}{
		{
			"empty",
			core.RateLimitRule{},
			`{"label":"","audience":"","duration":0,"maxRequests":0}`,
		},
		{
			"all fields",
			core.RateLimitRule{
				Label:       "POST /a/b/",
				Duration:    1,
				MaxRequests: 2,
				Audience:    core.RateLimitRuleAudienceAuth,
			},
			`{"label":"POST /a/b/","audience":"@auth","duration":1,"maxRequests":2}`,
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			result := s.rule.String()

			if result != s.expected {
				t.Fatalf("Expected string\n%s\ngot\n%s", s.expected, result)
			}
		})
	}
}
