using System.Net;
using System.Net.Http;
using JobTrackerApi.Services.JobImport;
using JobTrackerApi.Services.JobImport.Translation;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class JobImportServiceTests
{
    [Fact]
    public async Task Preview_rejects_hostname_that_resolves_to_loopback()
    {
        var resolver = new Mock<IHostAddressResolver>();
        resolver
            .Setup(x => x.ResolveAsync("127.0.0.1.nip.io", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[] { IPAddress.Loopback });

        var service = CreateService(resolver.Object);

        var result = await service.PreviewAsync("http://127.0.0.1.nip.io:5202/api/auth/config", CancellationToken.None);

        Assert.False(result.Success);
        Assert.Equal("none", result.Parser);
        Assert.Equal("Local or private network URLs are not allowed.", result.Error);
    }

    [Fact]
    public async Task Preview_rejects_hostname_that_resolves_to_private_ip()
    {
        var resolver = new Mock<IHostAddressResolver>();
        resolver
            .Setup(x => x.ResolveAsync("internal.example.test", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[] { IPAddress.Parse("10.10.1.5") });

        var service = CreateService(resolver.Object);

        var result = await service.PreviewAsync("https://internal.example.test/job/123", CancellationToken.None);

        Assert.False(result.Success);
        Assert.Equal("Local or private network URLs are not allowed.", result.Error);
    }

    [Fact]
    public async Task Preview_allows_public_hostname_resolution_and_fetches_html()
    {
        var resolver = new Mock<IHostAddressResolver>();
        resolver
            .Setup(x => x.ResolveAsync("example.com", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[] { IPAddress.Parse("93.184.216.34") });

        var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("<html><body>no schema</body></html>")
        });

        var service = CreateService(resolver.Object, handler);

        var result = await service.PreviewAsync("https://example.com/job", CancellationToken.None);

        Assert.False(result.Success);
        Assert.Equal("universal", result.Parser);
        Assert.Equal("No JobPosting schema found.", result.Error);
    }

    [Fact]
    public async Task Preview_stops_reading_a_chunked_response_at_the_download_limit()
    {
        var resolver = new Mock<IHostAddressResolver>();
        resolver.Setup(x => x.ResolveAsync("example.com", It.IsAny<CancellationToken>()))
            .ReturnsAsync([IPAddress.Parse("93.184.216.34")]);
        var contentStream = new GeneratedStream(10_000_000);
        var handler = new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StreamContent(contentStream)
        });
        var service = CreateService(resolver.Object, handler);

        var result = await service.PreviewAsync("https://example.com/job", CancellationToken.None);

        Assert.False(result.Success);
        Assert.Equal("fetch", result.Parser);
        Assert.InRange(contentStream.BytesRead, 4_000_001, 4_065_536);
    }

    private static JobImportService CreateService(IHostAddressResolver resolver, HttpMessageHandler? handler = null)
    {
        handler ??= new StubHttpMessageHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("<html></html>")
        });

        var httpClient = new HttpClient(handler, disposeHandler: true);
        var factory = new Mock<IHttpClientFactory>();
        factory.Setup(x => x.CreateClient("jobimport")).Returns(httpClient);

        return new JobImportService(
            factory.Object,
            new UniversalJobParser(),
            Array.Empty<IJobSitePlugin>(),
            new NoOpTranslationService(),
            resolver);
    }

    private sealed class StubHttpMessageHandler : HttpMessageHandler
    {
        private readonly Func<HttpRequestMessage, HttpResponseMessage> _handler;

        public StubHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> handler)
        {
            _handler = handler;
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
            => Task.FromResult(_handler(request));
    }

    private sealed class GeneratedStream(long length) : Stream
    {
        private long _position;
        public long BytesRead { get; private set; }
        public override bool CanRead => true;
        public override bool CanSeek => false;
        public override bool CanWrite => false;
        public override long Length => length;
        public override long Position { get => _position; set => throw new NotSupportedException(); }
        public override void Flush() { }
        public override int Read(byte[] buffer, int offset, int count)
        {
            var read = (int)Math.Min(count, length - _position);
            if (read <= 0) return 0;
            Array.Fill<byte>(buffer, (byte)'x', offset, read);
            _position += read;
            BytesRead += read;
            return read;
        }
        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
        {
            var read = (int)Math.Min(buffer.Length, length - _position);
            if (read <= 0) return ValueTask.FromResult(0);
            buffer.Span[..read].Fill((byte)'x');
            _position += read;
            BytesRead += read;
            return ValueTask.FromResult(read);
        }
        public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
        public override void SetLength(long value) => throw new NotSupportedException();
        public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
    }
}
