using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using MarcusLaw.OutlookAddin.Core.Models;
using MarcusLaw.OutlookAddin.Core.Services;
using Moq;
using Moq.Protected;
using Serilog;
using Xunit;

namespace MarcusLaw.OutlookAddin.Tests.Services
{
    public class EspoCrmClientTests
    {
        private const string BaseUrl = "https://crm.test.example/";
        private const string Username = "lawyer";
        private const string ApiKey = "secret-key";

        private static (EspoCrmClient client, Mock<HttpMessageHandler> handler) BuildClient(
            Func<HttpRequestMessage, HttpResponseMessage> respond)
        {
            var handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
            handler.Protected()
                .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
                .ReturnsAsync((HttpRequestMessage req, CancellationToken ct) => respond(req));

            var httpClient = new HttpClient(handler.Object);
            var logger = new LoggerConfiguration().CreateLogger();
            var client = new EspoCrmClient(httpClient, BaseUrl, Username, ApiKey, logger);
            return (client, handler);
        }

        private static HttpResponseMessage Json(HttpStatusCode code, object body)
        {
            return new HttpResponseMessage(code)
            {
                Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json")
            };
        }

        [Fact]
        public async Task FileMailAsync_HappyPath_PostsAndReturnsResponse()
        {
            var (client, handler) = BuildClient(req =>
            {
                req.Method.Should().Be(HttpMethod.Post);
                req.RequestUri!.AbsoluteUri.Should().Be("https://crm.test.example/api/v1/MailRouter/file");
                return Json(HttpStatusCode.OK, new FileEmailResponse
                {
                    Id = "abc",
                    Created = true,
                    ParentType = "Case",
                    ParentId = "case-1",
                    MessageId = "<mid@server>"
                });
            });

            var result = await client.FileMailAsync(new FileEmailRequest
            {
                Subject = "Hi",
                From = "a@b.c",
                ParentType = "Case",
                ParentId = "case-1",
                MessageId = "<mid@server>"
            });

            result.Id.Should().Be("abc");
            result.Created.Should().BeTrue();
            result.ParentType.Should().Be("Case");
        }

        [Fact]
        public async Task FileMailAsync_AddsBothApiKeyAndBasicAuthHeaders()
        {
            HttpRequestMessage? captured = null;
            var (client, _) = BuildClient(req =>
            {
                captured = req;
                return Json(HttpStatusCode.OK, new FileEmailResponse { Id = "x", Created = true });
            });

            await client.FileMailAsync(new FileEmailRequest { ParentType = "Case", ParentId = "c1" });

            // Primary auth — API-User type expects X-Api-Key
            captured!.Headers.GetValues("X-Api-Key").Should().ContainSingle().Which.Should().Be(ApiKey);
            // Fallback for legacy / regular users — Basic in Espo-Authorization
            var expectedBasic = Convert.ToBase64String(Encoding.UTF8.GetBytes(Username + ":" + ApiKey));
            captured.Headers.GetValues("Espo-Authorization").Should().ContainSingle().Which.Should().Be(expectedBasic);
        }

        [Fact]
        public async Task FileMailAsync_409Conflict_ReturnsNotCreatedWithoutThrowing()
        {
            var (client, _) = BuildClient(_ => Json(HttpStatusCode.Conflict, new FileEmailResponse
            {
                Id = "existing",
                Created = false,
                ParentType = "Case",
                ParentId = "case-1",
                MessageId = "<mid@server>"
            }));

            var result = await client.FileMailAsync(new FileEmailRequest
            {
                ParentType = "Case",
                ParentId = "case-1",
                MessageId = "<mid@server>"
            });

            result.Id.Should().Be("existing");
            result.Created.Should().BeFalse();
        }

        [Fact]
        public async Task FileMailAsync_409WithEmptyBody_ReturnsSyntheticResponse()
        {
            var (client, _) = BuildClient(_ => new HttpResponseMessage(HttpStatusCode.Conflict)
            {
                Content = new StringContent(string.Empty, Encoding.UTF8, "application/json")
            });

            var result = await client.FileMailAsync(new FileEmailRequest
            {
                ParentType = "Case",
                ParentId = "case-1",
                MessageId = "<mid@server>"
            });

            result.Created.Should().BeFalse();
            result.ParentType.Should().Be("Case");
            result.ParentId.Should().Be("case-1");
            result.MessageId.Should().Be("<mid@server>");
        }

        [Fact]
        public async Task Any401_ThrowsAuthorizationException()
        {
            var (client, _) = BuildClient(_ => new HttpResponseMessage(HttpStatusCode.Unauthorized)
            {
                Content = new StringContent("nope")
            });

            await Assert.ThrowsAsync<EspoCrmAuthorizationException>(() =>
                client.GetCaseAsync("case-1"));
        }

        [Fact]
        public async Task TransientServerError_RetriesThenSucceeds()
        {
            var attempts = 0;
            var (client, handler) = BuildClient(_ =>
            {
                attempts++;
                if (attempts < 3)
                {
                    return new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent("boom")
                    };
                }
                return Json(HttpStatusCode.OK, new UserInfo { Id = "u1", UserName = "lawyer" });
            });

            var result = await client.TestConnectionAsync();

            result.Id.Should().Be("u1");
            attempts.Should().Be(3);
        }

        [Fact]
        public async Task PersistentServerError_ExhaustsRetriesThenThrows()
        {
            var (client, _) = BuildClient(_ => new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("permanent failure")
            });

            var act = () => client.GetCaseAsync("case-1");
            (await act.Should().ThrowAsync<EspoCrmHttpException>())
                .Which.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
        }

        [Fact]
        public async Task GlobalSearch_BuildsUrlWithEncodedQuery()
        {
            HttpRequestMessage? captured = null;
            var (client, _) = BuildClient(req =>
            {
                captured = req;
                return Json(HttpStatusCode.OK, new SearchResult { Total = 0 });
            });

            await client.GlobalSearchAsync("Smith Family");

            captured!.RequestUri!.AbsoluteUri.Should().Be("https://crm.test.example/api/v1/GlobalSearch?q=Smith%20Family");
        }

        [Fact]
        public async Task EmailAddressSearch_AppendsEntityType()
        {
            HttpRequestMessage? captured = null;
            var (client, _) = BuildClient(req =>
            {
                captured = req;
                return Json(HttpStatusCode.OK, new SearchResult { Total = 0 });
            });

            await client.EmailAddressSearchAsync("a@b.c", "Contact");

            captured!.RequestUri!.AbsoluteUri.Should().Contain("EmailAddress/search?q=a%40b.c&entityType=Contact");
        }

        [Fact]
        public void Constructor_RejectsBlankCredentials()
        {
            var http = new HttpClient(new Mock<HttpMessageHandler>().Object);
            var logger = new LoggerConfiguration().CreateLogger();

            // Username is now optional (API key alone is sufficient when EspoCRM
            // user is type "api"); blank apiKey or baseUrl still throw.
            Action a2 = () => new EspoCrmClient(http, BaseUrl, Username, "", logger);
            Action a3 = () => new EspoCrmClient(http, "", Username, ApiKey, logger);
            Action a4 = () => new EspoCrmClient(http, BaseUrl, "", ApiKey, logger); // empty username — OK

            a2.Should().Throw<ArgumentException>();
            a3.Should().Throw<ArgumentException>();
            a4.Should().NotThrow();
        }
    }
}
