package server

import (
	"io"
	"net/http"
	"net/http/httptest"
	"sync/atomic"
	"testing"
	"text/template"

	"github.com/stretchr/testify/require"
	"heckel.io/ntfy/v2/user"
	"heckel.io/ntfy/v2/util"
)

func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
	forEachBackend(t, func(t *testing.T, databaseURL string) {
		var called, verified atomic.Bool
		var code atomic.Pointer[string]
		twilioVerifyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			body, err := io.ReadAll(r.Body)
			require.Nil(t, err)
			require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
			if r.URL.Path == "/v2/Services/VA1234567890/Verifications" {
				if code.Load() != nil {
					t.Fatal("Should be only called once")
				}
				require.Equal(t, "Channel=sms&To=%2B12223334444", string(body))
				code.Store(util.String("123456"))
			} else if r.URL.Path == "/v2/Services/VA1234567890/VerificationCheck" {
				if verified.Load() {
					t.Fatal("Should be only called once")
				}
				require.Equal(t, "Code=123456&To=%2B12223334444", string(body))
				verified.Store(true)
			} else {
				t.Fatal("Unexpected path:", r.URL.Path)
			}
		}))
		defer twilioVerifyServer.Close()
		twilioCallsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if called.Load() {
				t.Fatal("Should be only called once")
			}
			body, err := io.ReadAll(r.Body)
			require.Nil(t, err)
			require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
			require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
			require.Equal(t, "From=%2B1234567890&To=%2B12223334444&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
			called.Store(true)
		}))
		defer twilioCallsServer.Close()

		c := newTestConfigWithAuthFile(t, databaseURL)
		c.TwilioVerifyBaseURL = twilioVerifyServer.URL
		c.TwilioCallsBaseURL = twilioCallsServer.URL
		c.TwilioAccount = "AC1234567890"
		c.TwilioAuthToken = "AAEAA1234567890"
		c.TwilioPhoneNumber = "+1234567890"
		c.TwilioVerifyService = "VA1234567890"
		s := newTestServer(t, c)

		// Add tier and user
		require.Nil(t, s.userManager.AddTier(&user.Tier{
			Code:         "pro",
			MessageLimit: 10,
			CallLimit:    1,
		}))
		require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
		require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
		u, err := s.userManager.User("phil")
		require.Nil(t, err)

		// Send verification code for phone number
		response := request(t, s, "PUT", "/v1/account/phone/verify", `{"number":"+12223334444","channel":"sms"}`, map[string]string{
			"authorization": util.BasicAuth("phil", "phil"),
		})
		require.Equal(t, 200, response.Code)
		waitFor(t, func() bool {
			return *code.Load() == "123456"
		})

		// Add phone number with code
		response = request(t, s, "PUT", "/v1/account/phone", `{"number":"+12223334444","code":"123456"}`, map[string]string{
			"authorization": util.BasicAuth("phil", "phil"),
		})
		require.Equal(t, 200, response.Code)
		waitFor(t, func() bool {
			return verified.Load()
		})
		phoneNumbers, err := s.userManager.PhoneNumbers(u.ID)
		require.Nil(t, err)
		require.Equal(t, 1, len(phoneNumbers))
		require.Equal(t, "+12223334444", phoneNumbers[0])

		// Do the thing
		response = request(t, s, "POST", "/mytopic", "hi there", map[string]string{
			"authorization": util.BasicAuth("phil", "phil"),
			"x-call":        "yes",
		})
		require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
		waitFor(t, func() bool {
			return called.Load()
		})

		// Remove the phone number
		response = request(t, s, "DELETE", "/v1/account/phone", `{"number":"+12223334444"}`, map[string]string{
			"authorization": util.BasicAuth("phil", "phil"),
		})
		require.Equal(t, 200, response.Code)

		// Verify the phone number is gone from the DB
		phoneNumbers, err = s.userManager.PhoneNumbers(u.ID)
		require.Nil(t, err)
		require.Equal(t, 0, len(phoneNumbers))
	})
}

func TestServer_Twilio_Call_Success(t *testing.T) {
	forEachBackend(t, func(t *testing.T, databaseURL string) {
		var called atomic.Bool
		twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if called.Load() {
				t.Fatal("Should be only called once")
			}
			body, err := io.ReadAll(r.Body)
			require.Nil(t, err)
			require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
			require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
			require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
			called.Store(true)
		}))
		defer twilioServer.Close()

		c := newTestConfigWithAuthFile(t, databaseURL)
		c.TwilioCallsBaseURL = twilioServer.URL
		c.TwilioAccount = "AC1234567890"
		c.TwilioAuthToken = "AAEAA1234567890"
		c.TwilioPhoneNumber = "+1234567890"
		s := newTestServer(t, c)

		// Add tier and user
		require.Nil(t, s.userManager.AddTier(&user.Tier{
			Code:         "pro",
			MessageLimit: 10,
			CallLimit:    1,
		}))
		require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
		require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
		u, err := s.userManager.User("phil")
		require.Nil(t, err)
		require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))

		// Do the thing
		response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
			"authorization": util.BasicAuth("phil", "phil"),
			"x-call":        "+11122233344",
		})
		require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
		waitFor(t, func() bool {
			return called.Load()
		})
	})
}

func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
	forEachBackend(t, func(t *testing.T, databaseURL string) {
		var called atomic.Bool
		twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if called.Load() {
				t.Fatal("Should be only called once")
			}
			body, err := io.ReadAll(r.Body)
			require.Nil(t, err)
			require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
			require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
			require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+loop%3D%223%22%3E%0A%09%09You+have+a+message+from+notify+on+topic+mytopic.+Message%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09End+of+message.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09This+message+was+sent+by+user+phil.+It+will+be+repeated+three+times.%0A%09%09To+unsubscribe+from+calls+like+this%2C+remove+your+phone+number+in+the+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay%3EGoodbye.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
			called.Store(true)
		}))
		defer twilioServer.Close()

		c := newTestConfigWithAuthFile(t, databaseURL)
		c.TwilioCallsBaseURL = twilioServer.URL
		c.TwilioAccount = "AC1234567890"
		c.TwilioAuthToken = "AAEAA1234567890"
		c.TwilioPhoneNumber = "+1234567890"
		s := newTestServer(t, c)

		// Add tier and user
		require.Nil(t, s.userManager.AddTier(&user.Tier{
			Code:         "pro",
			MessageLimit: 10,
			CallLimit:    1,
		}))
		require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
		require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
		u, err := s.userManager.User("phil")
		require.Nil(t, err)
		require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))

		// Do the thing
		response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
			"authorization": util.BasicAuth("phil", "phil"),
			"x-call":        "yes", // <<<------
		})
		require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
		waitFor(t, func() bool {
			return called.Load()
		})
	})
}

func TestServer_Twilio_Call_Success_with_custom_twiml(t *testing.T) {
	forEachBackend(t, func(t *testing.T, databaseURL string) {
		var called atomic.Bool
		twilioServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if called.Load() {
				t.Fatal("Should be only called once")
			}
			body, err := io.ReadAll(r.Body)
			require.Nil(t, err)
			require.Equal(t, "/2010-04-01/Accounts/AC1234567890/Calls.json", r.URL.Path)
			require.Equal(t, "Basic QUMxMjM0NTY3ODkwOkFBRUFBMTIzNDU2Nzg5MA==", r.Header.Get("Authorization"))
			require.Equal(t, "From=%2B1234567890&To=%2B11122233344&Twiml=%0A%3CResponse%3E%0A%09%3CPause+length%3D%221%22%2F%3E%0A%09%3CSay+language%3D%22de-DE%22+loop%3D%223%22%3E%0A%09%09Du+hast+eine+Nachricht+von+notify+im+Thema+mytopic.+Nachricht%3A%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09hi+there%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09Ende+der+Nachricht.%0A%09%09%3Cbreak+time%3D%221s%22%2F%3E%0A%09%09Diese+Nachricht+wurde+von+Benutzer+phil+gesendet.+Sie+wird+drei+Mal+wiederholt.%0A%09%09Um+dich+von+Anrufen+wie+diesen+abzumelden%2C+entferne+deine+Telefonnummer+in+der+notify+web+app.%0A%09%09%3Cbreak+time%3D%223s%22%2F%3E%0A%09%3C%2FSay%3E%0A%09%3CSay+language%3D%22de-DE%22%3EAuf+Wiederh%C3%B6ren.%3C%2FSay%3E%0A%3C%2FResponse%3E", string(body))
			called.Store(true)
		}))
		defer twilioServer.Close()

		c := newTestConfigWithAuthFile(t, databaseURL)
		c.TwilioCallsBaseURL = twilioServer.URL
		c.TwilioAccount = "AC1234567890"
		c.TwilioAuthToken = "AAEAA1234567890"
		c.TwilioPhoneNumber = "+1234567890"
		c.TwilioCallFormat = template.Must(template.New("twiml").Parse(`
<Response>
	<Pause length="1"/>
	<Say language="de-DE" loop="3">
		Du hast eine Nachricht von notify im Thema {{.Topic}}. Nachricht:
		<break time="1s"/>
		{{.Message}}
		<break time="1s"/>
		Ende der Nachricht.
		<break time="1s"/>
		Diese Nachricht wurde von Benutzer {{.Sender}} gesendet. Sie wird drei Mal wiederholt.
		Um dich von Anrufen wie diesen abzumelden, entferne deine Telefonnummer in der notify web app.
		<break time="3s"/>
	</Say>
	<Say language="de-DE">Auf Wiederhören.</Say>
</Response>`))
		s := newTestServer(t, c)

		// Add tier and user
		require.Nil(t, s.userManager.AddTier(&user.Tier{
			Code:         "pro",
			MessageLimit: 10,
			CallLimit:    1,
		}))
		require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
		require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
		u, err := s.userManager.User("phil")
		require.Nil(t, err)
		require.Nil(t, s.userManager.AddPhoneNumber(u.ID, "+11122233344"))

		// Do the thing
		response := request(t, s, "POST", "/mytopic", "hi there", map[string]string{
			"authorization": util.BasicAuth("phil", "phil"),
			"x-call":        "+11122233344",
		})
		require.Equal(t, "hi there", toMessage(t, response.Body.String()).Message)
		waitFor(t, func() bool {
			return called.Load()
		})
	})
}

func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
	forEachBackend(t, func(t *testing.T, databaseURL string) {
		c := newTestConfigWithAuthFile(t, databaseURL)
		c.TwilioCallsBaseURL = "http://dummy.invalid"
		c.TwilioAccount = "AC1234567890"
		c.TwilioAuthToken = "AAEAA1234567890"
		c.TwilioPhoneNumber = "+1234567890"
		s := newTestServer(t, c)

		// Add tier and user
		require.Nil(t, s.userManager.AddTier(&user.Tier{
			Code:         "pro",
			MessageLimit: 10,
			CallLimit:    1,
		}))
		require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
		require.Nil(t, s.userManager.ChangeTier("phil", "pro"))

		// Do the thing
		response := request(t, s, "POST", "/mytopic", "test", map[string]string{
			"authorization": util.BasicAuth("phil", "phil"),
			"x-call":        "+11122233344",
		})
		require.Equal(t, 40034, toHTTPError(t, response.Body.String()).Code)
	})
}

func TestServer_Twilio_Call_InvalidNumber(t *testing.T) {
	forEachBackend(t, func(t *testing.T, databaseURL string) {
		c := newTestConfigWithAuthFile(t, databaseURL)
		c.TwilioCallsBaseURL = "https://127.0.0.1"
		c.TwilioAccount = "AC1234567890"
		c.TwilioAuthToken = "AAEAA1234567890"
		c.TwilioPhoneNumber = "+1234567890"
		s := newTestServer(t, c)

		response := request(t, s, "POST", "/mytopic", "test", map[string]string{
			"x-call": "+invalid",
		})
		require.Equal(t, 40033, toHTTPError(t, response.Body.String()).Code)
	})
}

func TestServer_Twilio_Call_Anonymous(t *testing.T) {
	forEachBackend(t, func(t *testing.T, databaseURL string) {
		c := newTestConfigWithAuthFile(t, databaseURL)
		c.TwilioCallsBaseURL = "https://127.0.0.1"
		c.TwilioAccount = "AC1234567890"
		c.TwilioAuthToken = "AAEAA1234567890"
		c.TwilioPhoneNumber = "+1234567890"
		s := newTestServer(t, c)

		response := request(t, s, "POST", "/mytopic", "test", map[string]string{
			"x-call": "+123123",
		})
		require.Equal(t, 40035, toHTTPError(t, response.Body.String()).Code)
	})
}

func TestServer_Twilio_Call_Unconfigured(t *testing.T) {
	forEachBackend(t, func(t *testing.T, databaseURL string) {
		s := newTestServer(t, newTestConfig(t, databaseURL))
		response := request(t, s, "POST", "/mytopic", "test", map[string]string{
			"x-call": "+1234",
		})
		require.Equal(t, 40032, toHTTPError(t, response.Body.String()).Code)
	})
}
