package tracker

import (
	"bytes"
	"context"
	"encoding/binary"
	"io"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"github.com/veggiedefender/torrent-client/internal/torrentfile"
)

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

	raw := []byte{127, 0, 0, 1, 0x1A, 0xE1}
	peers, err := parsePeers(raw)
	if err != nil {
		t.Fatalf("parsePeers returned error: %v", err)
	}
	if len(peers) != 1 {
		t.Fatalf("unexpected peer count: got %d", len(peers))
	}
	if got, want := peers[0].IP.String(), "127.0.0.1"; got != want {
		t.Fatalf("unexpected peer IP: got %s want %s", got, want)
	}
	if got, want := peers[0].Port, uint16(6881); got != want {
		t.Fatalf("unexpected peer port: got %d want %d", got, want)
	}
}

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

	_, err := parsePeers([]byte{1, 2, 3, 4, 5})
	if err == nil {
		t.Fatal("expected parsePeers to fail for invalid compact peer data")
	}
}

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

	var infoHash [20]byte
	copy(infoHash[:], []byte("abcdefghijklmnopqrst"))

	var peerID [20]byte
	copy(peerID[:], []byte("-ZT0001-123456789012"))

	tf := &torrentfile.TorrentFile{
		Announce: "http://tracker.test/announce",
		InfoHash: infoHash,
		Length:   1000,
	}

	var seenRequest *http.Request
	client := fakeClient{
		do: func(req *http.Request) (*http.Response, error) {
			seenRequest = req
			payload := []byte("d8:intervali1800e5:peers6:\x7f\x00\x00\x01\x1a\xe1e")
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader(payload)),
			}, nil
		},
	}

	announceURL, err := url.Parse(tf.Announce)
	if err != nil {
		t.Fatalf("failed to parse announce URL: %v", err)
	}

	peers, err := getPeersWithClient(context.Background(), client, announceURL, tf, AnnounceOptions{
		PeerID:     peerID,
		Port:       6881,
		Downloaded: 100,
	})
	if err != nil {
		t.Fatalf("GetPeers returned error: %v", err)
	}

	if seenRequest == nil {
		t.Fatal("expected tracker request to be sent")
	}

	q := seenRequest.URL.Query()
	if got := q.Get("compact"); got != "1" {
		t.Fatalf("unexpected compact value: got %q", got)
	}
	if got := q.Get("port"); got != "6881" {
		t.Fatalf("unexpected port value: got %q", got)
	}
	if got := q.Get("left"); got != "900" {
		t.Fatalf("unexpected left value: got %q", got)
	}
	if got := q.Get("numwant"); got != "50" {
		t.Fatalf("unexpected numwant value: got %q", got)
	}
	if got := []byte(q.Get("info_hash")); !equalBytes(got, infoHash[:]) {
		t.Fatalf("unexpected info_hash value: got %x want %x", got, infoHash)
	}
	if got := []byte(q.Get("peer_id")); !equalBytes(got, peerID[:]) {
		t.Fatalf("unexpected peer_id value: got %x want %x", got, peerID)
	}

	if len(peers) != 1 {
		t.Fatalf("unexpected peer count: got %d", len(peers))
	}
	if got, want := peers[0].Port, uint16(6881); got != want {
		t.Fatalf("unexpected peer port: got %d want %d", got, want)
	}
}

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

	var infoHash [20]byte
	copy(infoHash[:], []byte{0x00, 0xFF, 0x2F, 'A', 0x20})

	var peerID [20]byte
	copy(peerID[:], []byte("-ZT0001-\x00\xFFabc"))

	tf := &torrentfile.TorrentFile{
		Announce: "http://tracker.test/announce?token=abc",
		InfoHash: infoHash,
		Length:   10,
	}

	baseURL, err := url.Parse(tf.Announce)
	if err != nil {
		t.Fatalf("failed to parse announce URL: %v", err)
	}

	announceURL, err := buildAnnounceURL(baseURL, tf, AnnounceOptions{
		PeerID:     peerID,
		Port:       6881,
		NumWant:    42,
		Uploaded:   -5,
		Downloaded: -7,
	})
	if err != nil {
		t.Fatalf("buildAnnounceURL returned error: %v", err)
	}

	if !strings.Contains(announceURL, "token=abc") {
		t.Fatalf("announce URL did not preserve existing query params: %s", announceURL)
	}
	if !strings.Contains(announceURL, "info_hash=%00%FF%2FA%20") {
		t.Fatalf("announce URL did not encode binary info hash correctly: %s", announceURL)
	}
	if !strings.Contains(announceURL, "peer_id=-ZT0001-%00%FFabc") {
		t.Fatalf("announce URL did not encode binary peer ID correctly: %s", announceURL)
	}
	if !strings.Contains(announceURL, "uploaded=0") {
		t.Fatalf("announce URL did not clamp uploaded value: %s", announceURL)
	}
	if !strings.Contains(announceURL, "downloaded=0") {
		t.Fatalf("announce URL did not clamp downloaded value: %s", announceURL)
	}
	if !strings.Contains(announceURL, "left=10") {
		t.Fatalf("announce URL did not compute left bytes correctly: %s", announceURL)
	}
}

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

	baseURL, err := url.Parse("udp://tracker.test:6969/announce")
	if err != nil {
		t.Fatalf("failed to parse announce URL: %v", err)
	}

	_, err = buildAnnounceURL(baseURL, &torrentfile.TorrentFile{
		Length: 10,
	}, AnnounceOptions{})
	if err == nil {
		t.Fatal("expected buildAnnounceURL to reject unsupported tracker scheme")
	}
}

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

	tf := &torrentfile.TorrentFile{
		Announce: "http://tracker.test/announce",
		Length:   10,
	}

	client := fakeClient{
		do: func(req *http.Request) (*http.Response, error) {
			payload := []byte("d14:failure reason11:bad requeste")
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader(payload)),
			}, nil
		},
	}

	announceURL, err := url.Parse(tf.Announce)
	if err != nil {
		t.Fatalf("failed to parse announce URL: %v", err)
	}

	_, err = getPeersWithClient(context.Background(), client, announceURL, tf, AnnounceOptions{})
	if err == nil {
		t.Fatal("expected getPeersWithClient to return tracker failure error")
	}
}

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

	transactionID := uint32(42)
	connectionID := uint64(0x0102030405060708)

	payload := make([]byte, 16)
	binary.BigEndian.PutUint32(payload[0:4], 0)
	binary.BigEndian.PutUint32(payload[4:8], transactionID)
	binary.BigEndian.PutUint64(payload[8:16], connectionID)

	got, err := parseUDPConnectResponse(payload, transactionID)
	if err != nil {
		t.Fatalf("parseUDPConnectResponse returned error: %v", err)
	}
	if got != connectionID {
		t.Fatalf("unexpected connection ID: got %x want %x", got, connectionID)
	}
}

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

	transactionID := uint32(99)
	payload := make([]byte, 20+6)
	binary.BigEndian.PutUint32(payload[0:4], 1)
	binary.BigEndian.PutUint32(payload[4:8], transactionID)
	binary.BigEndian.PutUint32(payload[8:12], 1800)
	binary.BigEndian.PutUint32(payload[12:16], 10)
	binary.BigEndian.PutUint32(payload[16:20], 20)
	copy(payload[20:], []byte{127, 0, 0, 1, 0x1A, 0xE1})

	peers, err := parseUDPAnnounceResponse(payload, transactionID)
	if err != nil {
		t.Fatalf("parseUDPAnnounceResponse returned error: %v", err)
	}
	if len(peers) != 1 {
		t.Fatalf("unexpected peer count: got %d", len(peers))
	}
	if got, want := peers[0].Port, uint16(6881); got != want {
		t.Fatalf("unexpected peer port: got %d want %d", got, want)
	}
}

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

	tf := &torrentfile.TorrentFile{
		Length: 100,
	}
	copy(tf.InfoHash[:], []byte("abcdefghijklmnopqrst"))

	var peerID [20]byte
	copy(peerID[:], []byte("-ZT0001-123456789012"))

	req, err := buildUDPAnnounceRequest(0x0102030405060708, 42, tf, AnnounceOptions{
		PeerID:     peerID,
		Port:       6881,
		Uploaded:   -1,
		Downloaded: 20,
		NumWant:    50,
	}, 0xAABBCCDD)
	if err != nil {
		t.Fatalf("buildUDPAnnounceRequest returned error: %v", err)
	}

	if len(req) != 98 {
		t.Fatalf("unexpected announce request length: got %d", len(req))
	}
	if got, want := binary.BigEndian.Uint32(req[8:12]), uint32(1); got != want {
		t.Fatalf("unexpected action: got %d want %d", got, want)
	}
	if got, want := binary.BigEndian.Uint64(req[64:72]), uint64(80); got != want {
		t.Fatalf("unexpected left bytes: got %d want %d", got, want)
	}
	if got, want := binary.BigEndian.Uint64(req[72:80]), uint64(0); got != want {
		t.Fatalf("unexpected uploaded bytes: got %d want %d", got, want)
	}
}

type fakeClient struct {
	do func(req *http.Request) (*http.Response, error)
}

func (c fakeClient) Do(req *http.Request) (*http.Response, error) {
	return c.do(req)
}

func equalBytes(a, b []byte) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
