package tailscale

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net"
	"testing"
	"time"

	"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"tailscale.com/ipn"
	"tailscale.com/ipn/ipnstate"
)

type mockConn struct {
	*bytes.Buffer
}

func (m *mockConn) Close() error                       { return nil }
func (m *mockConn) LocalAddr() net.Addr                { return nil }
func (m *mockConn) RemoteAddr() net.Addr               { return nil }
func (m *mockConn) SetDeadline(t time.Time) error      { return nil }
func (m *mockConn) SetReadDeadline(t time.Time) error  { return nil }
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }

func handlerTestManager() *Manager {
	client := &mockClient{
		watchFn: func(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) {
			<-ctx.Done()
			return nil, ctx.Err()
		},
		statusFn: func(ctx context.Context) (*ipnstate.Status, error) {
			return runningStatus(), nil
		},
	}
	m := newManager(client)
	m.RefreshState()
	return m
}

func TestHandleGetStatus(t *testing.T) {
	m := handlerTestManager()
	defer m.Close()

	buf := &bytes.Buffer{}
	conn := models.NewConn(&mockConn{Buffer: buf})

	req := models.Request{ID: 1, Method: "tailscale.getStatus"}
	handleGetStatus(conn, req, m)

	var resp models.Response[TailscaleState]
	err := json.NewDecoder(buf).Decode(&resp)
	require.NoError(t, err)
	assert.Equal(t, 1, resp.ID)
	assert.NotNil(t, resp.Result)
	assert.True(t, resp.Result.Connected)
	assert.Equal(t, "cachyos", resp.Result.Self.Hostname)
}

func TestHandleRefresh(t *testing.T) {
	m := handlerTestManager()
	defer m.Close()

	buf := &bytes.Buffer{}
	conn := models.NewConn(&mockConn{Buffer: buf})

	req := models.Request{ID: 1, Method: "tailscale.refresh"}
	handleRefresh(conn, req, m)

	var resp models.Response[models.SuccessResult]
	err := json.NewDecoder(buf).Decode(&resp)
	require.NoError(t, err)
	assert.Equal(t, 1, resp.ID)
	assert.NotNil(t, resp.Result)
	assert.True(t, resp.Result.Success)
}

func TestHandleActions(t *testing.T) {
	cases := []struct {
		name   string
		method string
		params map[string]any
	}{
		{"connect", "tailscale.connect", nil},
		{"disconnect", "tailscale.disconnect", nil},
		{"setExitNode", "tailscale.setExitNode", map[string]any{"id": "nABC123"}},
		{"clearExitNode", "tailscale.setExitNode", map[string]any{"id": ""}},
		{"setAllowLanAccess", "tailscale.setAllowLanAccess", map[string]any{"enabled": true}},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			m := handlerTestManager()
			defer m.Close()

			buf := &bytes.Buffer{}
			conn := models.NewConn(&mockConn{Buffer: buf})

			req := models.Request{ID: 1, Method: tc.method, Params: tc.params}
			HandleRequest(conn, req, m)

			var resp models.Response[models.SuccessResult]
			require.NoError(t, json.NewDecoder(buf).Decode(&resp))
			assert.Equal(t, 1, resp.ID)
			assert.Empty(t, resp.Error)
			require.NotNil(t, resp.Result)
			assert.True(t, resp.Result.Success)
		})
	}
}

func TestHandleAction_BackendError(t *testing.T) {
	client := &mockClient{
		watchFn:  blockingWatch,
		statusFn: func(ctx context.Context) (*ipnstate.Status, error) { return runningStatus(), nil },
		editPrefsFn: func(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
			return nil, fmt.Errorf("backend rejected edit")
		},
	}
	m := newManager(client)
	defer m.Close()

	buf := &bytes.Buffer{}
	conn := models.NewConn(&mockConn{Buffer: buf})

	req := models.Request{ID: 1, Method: "tailscale.connect"}
	HandleRequest(conn, req, m)

	var resp models.Response[models.SuccessResult]
	require.NoError(t, json.NewDecoder(buf).Decode(&resp))
	assert.Nil(t, resp.Result)
	assert.Contains(t, resp.Error, "backend rejected edit")
}

func TestHandleRequest_UnknownMethod(t *testing.T) {
	m := handlerTestManager()
	defer m.Close()

	buf := &bytes.Buffer{}
	conn := models.NewConn(&mockConn{Buffer: buf})

	req := models.Request{ID: 1, Method: "tailscale.unknownMethod"}
	HandleRequest(conn, req, m)

	var resp models.Response[any]
	err := json.NewDecoder(buf).Decode(&resp)
	require.NoError(t, err)
	assert.Nil(t, resp.Result)
	assert.NotEmpty(t, resp.Error)
	assert.Contains(t, resp.Error, "unknown method")
}
