using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Terminal.Gui;
using Xunit;
using System.Globalization;
using Xunit.Abstractions;
using System.Reflection;

namespace Terminal.Gui.ViewTests {

	public class TableViewTests {
		readonly ITestOutputHelper output;

		public TableViewTests (ITestOutputHelper output)
		{
			this.output = output;
		}
		[Fact]
		public void EnsureValidScrollOffsets_WithNoCells ()
		{
			var tableView = new TableView ();

			Assert.Equal (0, tableView.RowOffset);
			Assert.Equal (0, tableView.ColumnOffset);

			// Set empty table
			tableView.Table = new DataTable ();

			// Since table has no rows or columns scroll offset should default to 0
			tableView.EnsureValidScrollOffsets ();
			Assert.Equal (0, tableView.RowOffset);
			Assert.Equal (0, tableView.ColumnOffset);
		}



		[Fact]
		public void EnsureValidScrollOffsets_LoadSmallerTable ()
		{
			var tableView = new TableView ();
			tableView.Bounds = new Rect (0, 0, 25, 10);

			Assert.Equal (0, tableView.RowOffset);
			Assert.Equal (0, tableView.ColumnOffset);

			// Set big table
			tableView.Table = BuildTable (25, 50);

			// Scroll down and along
			tableView.RowOffset = 20;
			tableView.ColumnOffset = 10;

			tableView.EnsureValidScrollOffsets ();

			// The scroll should be valid at the moment
			Assert.Equal (20, tableView.RowOffset);
			Assert.Equal (10, tableView.ColumnOffset);

			// Set small table
			tableView.Table = BuildTable (2, 2);

			// Setting a small table should automatically trigger fixing the scroll offsets to ensure valid cells
			Assert.Equal (0, tableView.RowOffset);
			Assert.Equal (0, tableView.ColumnOffset);


			// Trying to set invalid indexes should not be possible
			tableView.RowOffset = 20;
			tableView.ColumnOffset = 10;

			Assert.Equal (1, tableView.RowOffset);
			Assert.Equal (1, tableView.ColumnOffset);
		}

		[Fact]
		[AutoInitShutdown]
		public void Redraw_EmptyTable ()
		{
			var tableView = new TableView ();
			tableView.ColorScheme = new ColorScheme();
			tableView.Bounds = new Rect (0, 0, 25, 10);

			// Set a table with 1 column
			tableView.Table = BuildTable (1, 50);
			tableView.Redraw(tableView.Bounds);

			tableView.Table.Columns.Remove(tableView.Table.Columns[0]);
			tableView.Redraw(tableView.Bounds);
		}


		[Fact]
		public void SelectedCellChanged_NotFiredForSameValue ()
		{
			var tableView = new TableView () {
				Table = BuildTable (25, 50)
			};

			bool called = false;
			tableView.SelectedCellChanged += (e) => { called = true; };

			Assert.Equal (0, tableView.SelectedColumn);
			Assert.False (called);

			// Changing value to same as it already was should not raise an event
			tableView.SelectedColumn = 0;

			Assert.False (called);

			tableView.SelectedColumn = 10;
			Assert.True (called);
		}



		[Fact]
		public void SelectedCellChanged_SelectedColumnIndexesCorrect ()
		{
			var tableView = new TableView () {
				Table = BuildTable (25, 50)
			};

			bool called = false;
			tableView.SelectedCellChanged += (e) => {
				called = true;
				Assert.Equal (0, e.OldCol);
				Assert.Equal (10, e.NewCol);
			};

			tableView.SelectedColumn = 10;
			Assert.True (called);
		}

		[Fact]
		public void SelectedCellChanged_SelectedRowIndexesCorrect ()
		{
			var tableView = new TableView () {
				Table = BuildTable (25, 50)
			};

			bool called = false;
			tableView.SelectedCellChanged += (e) => {
				called = true;
				Assert.Equal (0, e.OldRow);
				Assert.Equal (10, e.NewRow);
			};

			tableView.SelectedRow = 10;
			Assert.True (called);
		}

		[Fact]
		public void Test_SumColumnWidth_UnicodeLength ()
		{
			Assert.Equal (11, "hello there".Sum (c => Rune.ColumnWidth (c)));

			// Creates a string with the peculiar (french?) r symbol
			String surrogate = "Les Mise" + Char.ConvertFromUtf32 (Int32.Parse ("0301", NumberStyles.HexNumber)) + "rables";

			// The unicode width of this string is shorter than the string length! 
			Assert.Equal (14, surrogate.Sum (c => Rune.ColumnWidth (c)));
			Assert.Equal (15, surrogate.Length);
		}

		[Fact]
		public void IsSelected_MultiSelectionOn_Vertical ()
		{
			var tableView = new TableView () {
				Table = BuildTable (25, 50),
				MultiSelect = true
			};

			// 3 cell vertical selection
			tableView.SetSelection (1, 1, false);
			tableView.SetSelection (1, 3, true);

			Assert.False (tableView.IsSelected (0, 0));
			Assert.False (tableView.IsSelected (1, 0));
			Assert.False (tableView.IsSelected (2, 0));

			Assert.False (tableView.IsSelected (0, 1));
			Assert.True (tableView.IsSelected (1, 1));
			Assert.False (tableView.IsSelected (2, 1));

			Assert.False (tableView.IsSelected (0, 2));
			Assert.True (tableView.IsSelected (1, 2));
			Assert.False (tableView.IsSelected (2, 2));

			Assert.False (tableView.IsSelected (0, 3));
			Assert.True (tableView.IsSelected (1, 3));
			Assert.False (tableView.IsSelected (2, 3));

			Assert.False (tableView.IsSelected (0, 4));
			Assert.False (tableView.IsSelected (1, 4));
			Assert.False (tableView.IsSelected (2, 4));
		}


		[Fact]
		public void IsSelected_MultiSelectionOn_Horizontal ()
		{
			var tableView = new TableView () {
				Table = BuildTable (25, 50),
				MultiSelect = true
			};

			// 2 cell horizontal selection
			tableView.SetSelection (1, 0, false);
			tableView.SetSelection (2, 0, true);

			Assert.False (tableView.IsSelected (0, 0));
			Assert.True (tableView.IsSelected (1, 0));
			Assert.True (tableView.IsSelected (2, 0));
			Assert.False (tableView.IsSelected (3, 0));

			Assert.False (tableView.IsSelected (0, 1));
			Assert.False (tableView.IsSelected (1, 1));
			Assert.False (tableView.IsSelected (2, 1));
			Assert.False (tableView.IsSelected (3, 1));
		}



		[Fact]
		public void IsSelected_MultiSelectionOn_BoxSelection ()
		{
			var tableView = new TableView () {
				Table = BuildTable (25, 50),
				MultiSelect = true
			};

			// 4 cell horizontal in box 2x2
			tableView.SetSelection (0, 0, false);
			tableView.SetSelection (1, 1, true);

			Assert.True (tableView.IsSelected (0, 0));
			Assert.True (tableView.IsSelected (1, 0));
			Assert.False (tableView.IsSelected (2, 0));

			Assert.True (tableView.IsSelected (0, 1));
			Assert.True (tableView.IsSelected (1, 1));
			Assert.False (tableView.IsSelected (2, 1));

			Assert.False (tableView.IsSelected (0, 2));
			Assert.False (tableView.IsSelected (1, 2));
			Assert.False (tableView.IsSelected (2, 2));
		}

		[AutoInitShutdown]
		[Fact]
		public void PageDown_ExcludesHeaders ()
		{
			var tableView = new TableView () {
				Table = BuildTable (25, 50),
				MultiSelect = true,
				Bounds = new Rect (0, 0, 10, 5)
			};

			// Header should take up 2 lines
			tableView.Style.ShowHorizontalHeaderOverline = false;
			tableView.Style.ShowHorizontalHeaderUnderline = true;
			tableView.Style.AlwaysShowHeaders = false;

			// ensure that TableView has the input focus
			Application.Top.Add (tableView);
			Application.Top.FocusFirst ();
			Assert.True (tableView.HasFocus);

			Assert.Equal (0, tableView.RowOffset);

			tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()));

			// window height is 5 rows 2 are header so page down should give 3 new rows
			Assert.Equal (3, tableView.SelectedRow);
			Assert.Equal (1, tableView.RowOffset);

			// header is no longer visible so page down should give 5 new rows
			tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()));

			Assert.Equal (8, tableView.SelectedRow);
			Assert.Equal (4, tableView.RowOffset);
		}

		[Fact]
		public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun ()
		{
			// create a 4 by 4 table
			var tableView = new TableView () {
				Table = BuildTable (4, 4),
				MultiSelect = true,
				Bounds = new Rect (0, 0, 10, 5)
			};

			tableView.SelectAll ();
			Assert.Equal (16, tableView.GetAllSelectedCells ().Count ());

			// delete one of the columns
			tableView.Table.Columns.RemoveAt (2);

			// table should now be 3x4
			Assert.Equal (12, tableView.GetAllSelectedCells ().Count ());

			// remove a row
			tableView.Table.Rows.RemoveAt (1);

			// table should now be 3x3
			Assert.Equal (9, tableView.GetAllSelectedCells ().Count ());
		}


		[Fact]
		public void DeleteRow_SelectLastRow_AdjustsSelectionToPreventOverrun ()
		{
			// create a 4 by 4 table
			var tableView = new TableView () {
				Table = BuildTable (4, 4),
				MultiSelect = true,
				Bounds = new Rect (0, 0, 10, 5)
			};

			// select the last row
			tableView.MultiSelectedRegions.Clear ();
			tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (0, 3), new Rect (0, 3, 4, 1)));

			Assert.Equal (4, tableView.GetAllSelectedCells ().Count ());

			// remove a row
			tableView.Table.Rows.RemoveAt (0);

			tableView.EnsureValidSelection ();

			// since the selection no longer exists it should be removed
			Assert.Empty (tableView.MultiSelectedRegions);
		}

		[Theory]
		[InlineData (true)]
		[InlineData (false)]
		public void GetAllSelectedCells_SingleCellSelected_ReturnsOne (bool multiSelect)
		{
			var tableView = new TableView () {
				Table = BuildTable (3, 3),
				MultiSelect = multiSelect,
				Bounds = new Rect (0, 0, 10, 5)
			};

			tableView.SetSelection (1, 1, false);

			Assert.Single (tableView.GetAllSelectedCells ());
			Assert.Equal (new Point (1, 1), tableView.GetAllSelectedCells ().Single ());
		}


		[Fact]
		public void GetAllSelectedCells_SquareSelection_ReturnsFour ()
		{
			var tableView = new TableView () {
				Table = BuildTable (3, 3),
				MultiSelect = true,
				Bounds = new Rect (0, 0, 10, 5)
			};

			// move cursor to 1,1
			tableView.SetSelection (1, 1, false);
			// spread selection across to 2,2 (e.g. shift+right then shift+down)
			tableView.SetSelection (2, 2, true);

			var selected = tableView.GetAllSelectedCells ().ToArray ();

			Assert.Equal (4, selected.Length);
			Assert.Equal (new Point (1, 1), selected [0]);
			Assert.Equal (new Point (2, 1), selected [1]);
			Assert.Equal (new Point (1, 2), selected [2]);
			Assert.Equal (new Point (2, 2), selected [3]);
		}


		[Fact]
		public void GetAllSelectedCells_SquareSelection_FullRowSelect ()
		{
			var tableView = new TableView () {
				Table = BuildTable (3, 3),
				MultiSelect = true,
				FullRowSelect = true,
				Bounds = new Rect (0, 0, 10, 5)
			};

			// move cursor to 1,1
			tableView.SetSelection (1, 1, false);
			// spread selection across to 2,2 (e.g. shift+right then shift+down)
			tableView.SetSelection (2, 2, true);

			var selected = tableView.GetAllSelectedCells ().ToArray ();

			Assert.Equal (6, selected.Length);
			Assert.Equal (new Point (0, 1), selected [0]);
			Assert.Equal (new Point (1, 1), selected [1]);
			Assert.Equal (new Point (2, 1), selected [2]);
			Assert.Equal (new Point (0, 2), selected [3]);
			Assert.Equal (new Point (1, 2), selected [4]);
			Assert.Equal (new Point (2, 2), selected [5]);
		}


		[Fact]
		public void GetAllSelectedCells_TwoIsolatedSelections_ReturnsSix ()
		{
			var tableView = new TableView () {
				Table = BuildTable (20, 20),
				MultiSelect = true,
				Bounds = new Rect (0, 0, 10, 5)
			};

			/*  
				Sets up disconnected selections like:

				00000000000
				01100000000
				01100000000
				00000001100
				00000000000
			*/

			tableView.MultiSelectedRegions.Clear ();
			tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (1, 1), new Rect (1, 1, 2, 2)));
			tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (7, 3), new Rect (7, 3, 2, 1)));

			tableView.SelectedColumn = 8;
			tableView.SelectedRow = 3;

			var selected = tableView.GetAllSelectedCells ().ToArray ();

			Assert.Equal (6, selected.Length);

			Assert.Equal (new Point (1, 1), selected [0]);
			Assert.Equal (new Point (2, 1), selected [1]);
			Assert.Equal (new Point (1, 2), selected [2]);
			Assert.Equal (new Point (2, 2), selected [3]);
			Assert.Equal (new Point (7, 3), selected [4]);
			Assert.Equal (new Point (8, 3), selected [5]);
		}

		[Fact]
		public void TableView_ExpandLastColumn_True ()
		{
			var tv = SetUpMiniTable ();

			// the thing we are testing
			tv.Style.ExpandLastColumn = true;

			tv.Redraw (tv.Bounds);

			string expected = @"
┌─┬──────┐
│A│B     │
├─┼──────┤
│1│2     │
";
			TestHelpers.AssertDriverContentsAre (expected, output);

			// Shutdown must be called to safely clean up Application if Init has been called
			Application.Shutdown ();
		}


		[Fact]
		public void TableView_ExpandLastColumn_False ()
		{
			var tv = SetUpMiniTable ();

			// the thing we are testing
			tv.Style.ExpandLastColumn = false;

			tv.Redraw (tv.Bounds);

			string expected = @"
┌─┬─┬────┐
│A│B│    │
├─┼─┼────┤
│1│2│    │
";
			TestHelpers.AssertDriverContentsAre (expected, output);

			// Shutdown must be called to safely clean up Application if Init has been called
			Application.Shutdown ();
		}

		[Fact]
		public void TableView_ExpandLastColumn_False_ExactBounds ()
		{
			var tv = SetUpMiniTable ();

			// the thing we are testing
			tv.Style.ExpandLastColumn = false;
			// width exactly matches the max col widths
			tv.Bounds = new Rect (0, 0, 5, 4);

			tv.Redraw (tv.Bounds);

			string expected = @"
┌─┬─┐
│A│B│
├─┼─┤
│1│2│
";
			TestHelpers.AssertDriverContentsAre (expected, output);

			// Shutdown must be called to safely clean up Application if Init has been called
			Application.Shutdown ();
		}

		[Fact]
		[AutoInitShutdown]
		public void TableView_Activate()
		{
			string activatedValue = null;
			var tv = new TableView (BuildTable(1,1));
			tv.CellActivated += (c) => activatedValue = c.Table.Rows[c.Row][c.Col].ToString();

			Application.Top.Add (tv);
			Application.Begin (Application.Top);

			// pressing enter should activate the first cell (selected cell)
			tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()));
			Assert.Equal ("R0C0",activatedValue);

			// reset the test
			activatedValue = null;

			// clear keybindings and ensure that Enter does not trigger the event anymore
			tv.ClearKeybindings ();
			tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()));
			Assert.Null(activatedValue);

			// New method for changing the activation key
			tv.AddKeyBinding (Key.z, Command.Accept);
			tv.ProcessKey (new KeyEvent (Key.z, new KeyModifiers ()));
			Assert.Equal ("R0C0", activatedValue);

			// reset the test
			activatedValue = null;
			tv.ClearKeybindings ();

			// Old method for changing the activation key
			tv.CellActivationKey = Key.z;
			tv.ProcessKey (new KeyEvent (Key.z, new KeyModifiers ()));
			Assert.Equal ("R0C0", activatedValue);
		}

		[Fact]
		public void TableViewMultiSelect_CannotFallOffLeft()
		{
			var tv = SetUpMiniTable ();
			tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows)

			tv.MultiSelect = true;
			tv.SelectedColumn = 1;
			tv.SelectedRow = 1;
			tv.ProcessKey (new KeyEvent (Key.CursorLeft | Key.ShiftMask, new KeyModifiers { Shift = true }));

			Assert.Equal (new Rect (0, 1, 2, 1), tv.MultiSelectedRegions.Single().Rect);

			// this next shift left should be ignored because we are already at the bounds
			tv.ProcessKey (new KeyEvent (Key.CursorLeft | Key.ShiftMask, new KeyModifiers { Shift = true }));

			Assert.Equal (new Rect (0, 1, 2, 1), tv.MultiSelectedRegions.Single ().Rect);

			Assert.Equal (0, tv.SelectedColumn);
			Assert.Equal (1, tv.SelectedRow);

			Application.Shutdown ();
		}
		[Fact]
		public void TableViewMultiSelect_CannotFallOffRight()
		{
			var tv = SetUpMiniTable ();
			tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows)

			tv.MultiSelect = true;
			tv.SelectedColumn = 0;
			tv.SelectedRow = 1;
			tv.ProcessKey (new KeyEvent (Key.CursorRight | Key.ShiftMask, new KeyModifiers { Shift = true }));

			Assert.Equal (new Rect (0, 1, 2, 1), tv.MultiSelectedRegions.Single ().Rect);

			// this next shift right should be ignored because we are already at the right bounds
			tv.ProcessKey (new KeyEvent (Key.CursorRight | Key.ShiftMask, new KeyModifiers { Shift = true }));

			Assert.Equal (new Rect (0, 1, 2, 1), tv.MultiSelectedRegions.Single ().Rect);

			Assert.Equal (1, tv.SelectedColumn);
			Assert.Equal (1, tv.SelectedRow);

			Application.Shutdown ();
		}
		[Fact]
		public void TableViewMultiSelect_CannotFallOffBottom ()
		{
			var tv = SetUpMiniTable ();
			tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows)

			tv.MultiSelect = true;
			tv.SelectedColumn = 0;
			tv.SelectedRow = 0;
			tv.ProcessKey (new KeyEvent (Key.CursorRight | Key.ShiftMask, new KeyModifiers { Shift = true }));
			tv.ProcessKey (new KeyEvent (Key.CursorDown | Key.ShiftMask, new KeyModifiers { Shift = true }));

			Assert.Equal (new Rect (0, 0, 2, 2), tv.MultiSelectedRegions.Single ().Rect);

			// this next moves should be ignored because we already selected the whole table
			tv.ProcessKey (new KeyEvent (Key.CursorRight | Key.ShiftMask, new KeyModifiers { Shift = true }));
			tv.ProcessKey (new KeyEvent (Key.CursorDown | Key.ShiftMask, new KeyModifiers { Shift = true }));

			Assert.Equal (new Rect (0, 0, 2, 2), tv.MultiSelectedRegions.Single ().Rect);
			Assert.Equal (1, tv.SelectedColumn);
			Assert.Equal (1, tv.SelectedRow);

			Application.Shutdown ();
		}

		[Fact]
		public void TableViewMultiSelect_CannotFallOffTop()
		{
			var tv = SetUpMiniTable ();
			tv.Table.Rows.Add (1, 2); // add another row (brings us to 2 rows)

			tv.MultiSelect = true;
			tv.SelectedColumn = 1;
			tv.SelectedRow = 1;
			tv.ProcessKey (new KeyEvent (Key.CursorLeft | Key.ShiftMask, new KeyModifiers { Shift = true }));
			tv.ProcessKey (new KeyEvent (Key.CursorUp | Key.ShiftMask, new KeyModifiers { Shift = true }));

			Assert.Equal (new Rect (0, 0, 2, 2), tv.MultiSelectedRegions.Single ().Rect);

			// this next moves should be ignored because we already selected the whole table
			tv.ProcessKey (new KeyEvent (Key.CursorLeft | Key.ShiftMask, new KeyModifiers { Shift = true }));
			tv.ProcessKey (new KeyEvent (Key.CursorUp | Key.ShiftMask, new KeyModifiers { Shift = true }));

			Assert.Equal (new Rect (0, 0, 2, 2), tv.MultiSelectedRegions.Single ().Rect);
			Assert.Equal (0, tv.SelectedColumn);
			Assert.Equal (0, tv.SelectedRow);

			Application.Shutdown ();
		}

		[Fact, AutoInitShutdown]
		public void TestShiftClick_MultiSelect_TwoRowTable_FullRowSelect()
		{
			var tv = GetTwoRowSixColumnTable ();

			tv.MultiSelect = true;
			
			// Clicking in bottom row
			tv.MouseEvent (new MouseEvent {
				X = 1,
				Y = 3,
				Flags = MouseFlags.Button1Clicked
			});

			// should select that row
			Assert.Equal (1, tv.SelectedRow);

			// shift clicking top row
			tv.MouseEvent (new MouseEvent {
				X = 1,
				Y = 2,
				Flags = MouseFlags.Button1Clicked | MouseFlags.ButtonShift
			});

			// should extend the selection
			Assert.Equal (0, tv.SelectedRow);

			var selected = tv.GetAllSelectedCells ().ToArray();

			Assert.Contains (new Point(0,0), selected);
			Assert.Contains (new Point (0, 1), selected);
		}

		[Fact, AutoInitShutdown]
		public void TestControlClick_MultiSelect_ThreeRowTable_FullRowSelect ()
		{
			var tv = GetTwoRowSixColumnTable ();
			tv.Table.Rows.Add (1, 2, 3, 4, 5, 6);

			tv.MultiSelect = true;

			// Clicking in bottom row
			tv.MouseEvent (new MouseEvent {
				X = 1,
				Y = 4,
				Flags = MouseFlags.Button1Clicked
			});

			// should select that row
			Assert.Equal (2, tv.SelectedRow);

			// shift clicking top row
			tv.MouseEvent (new MouseEvent {
				X = 1,
				Y = 2,
				Flags = MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl
			});

			// should extend the selection
			// to include bottom and top row but not middle
			Assert.Equal (0, tv.SelectedRow);

			var selected = tv.GetAllSelectedCells ().ToArray ();

			Assert.Contains (new Point (0, 0), selected);
			Assert.DoesNotContain (new Point (0, 1), selected);
			Assert.Contains (new Point (0, 2), selected);
		}

		[Theory]
		[InlineData (false)]
		[InlineData (true)]
		public void TableView_ColorTests_FocusedOrNot (bool focused)
		{
			var tv = SetUpMiniTable ();

			// width exactly matches the max col widths
			tv.Bounds = new Rect (0, 0, 5, 4);

			// private method for forcing the view to be focused/not focused
			var setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic);

			// when the view is/isn't focused 
			setFocusMethod.Invoke (tv, new object [] { focused, tv, true });

			tv.Redraw (tv.Bounds);

			string expected = @"
┌─┬─┐
│A│B│
├─┼─┤
│1│2│
";
			TestHelpers.AssertDriverContentsAre (expected, output);


			string expectedColors = @"
00000
00000
00000
01000
";
			
			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
				// 0
				tv.ColorScheme.Normal,				
				// 1
				focused ? tv.ColorScheme.HotFocus : tv.ColorScheme.HotNormal});

			Application.Shutdown();
		}

		[Theory]
		[InlineData (false)]
		[InlineData (true)]
		public void TableView_ColorTests_InvertSelectedCellFirstCharacter (bool focused)
		{
			var tv = SetUpMiniTable ();
			tv.Style.InvertSelectedCellFirstCharacter = true;

			// width exactly matches the max col widths
			tv.Bounds = new Rect (0, 0, 5, 4);

			// private method for forcing the view to be focused/not focused
			var setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic);

			// when the view is/isn't focused 
			setFocusMethod.Invoke (tv, new object [] { focused, tv, true });

			tv.Redraw (tv.Bounds);

			string expected = @"
┌─┬─┐
│A│B│
├─┼─┤
│1│2│
";
			TestHelpers.AssertDriverContentsAre (expected, output);


			string expectedColors = @"
00000
00000
00000
01000
";
			
			var invertHotFocus = new Attribute(tv.ColorScheme.HotFocus.Background,tv.ColorScheme.HotFocus.Foreground);
			var invertHotNormal = new Attribute(tv.ColorScheme.HotNormal.Background,tv.ColorScheme.HotNormal.Foreground);

			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
				// 0
				tv.ColorScheme.Normal,				
				// 1
				focused ?  invertHotFocus : invertHotNormal});
			
			Application.Shutdown();
		}


		[Theory]
		[InlineData (false)]
		[InlineData (true)]
		public void TableView_ColorsTest_RowColorGetter (bool focused)
		{
			var tv = SetUpMiniTable ();

			// width exactly matches the max col widths
			tv.Bounds = new Rect (0, 0, 5, 4);

			var rowHighlight = new ColorScheme () {
				Normal = Attribute.Make (Color.BrightCyan, Color.DarkGray),
				HotNormal = Attribute.Make (Color.Green, Color.Blue),
				HotFocus = Attribute.Make (Color.BrightYellow, Color.White),
				Focus = Attribute.Make (Color.Cyan, Color.Magenta),
			};

			// when B is 2 use the custom highlight colour for the row
			tv.Style.RowColorGetter += (e)=>Convert.ToInt32(e.Table.Rows[e.RowIndex][1]) == 2 ? rowHighlight : null;

			// private method for forcing the view to be focused/not focused
			var setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic);

			// when the view is/isn't focused 
			setFocusMethod.Invoke (tv, new object [] { focused, tv, true });

			tv.Redraw (tv.Bounds);

			string expected = @"
┌─┬─┐
│A│B│
├─┼─┤
│1│2│
";
			TestHelpers.AssertDriverContentsAre (expected, output);


			string expectedColors = @"
00000
00000
00000
21222
";
			
			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
				// 0
				tv.ColorScheme.Normal,				
				// 1
				focused ? rowHighlight.HotFocus : rowHighlight.HotNormal,
				// 2
				rowHighlight.Normal});


			// change the value in the table so that
			// it no longer matches the RowColorGetter
			// delegate conditional ( which checks for
			// the value 2)
			tv.Table.Rows[0][1] = 5;

			tv.Redraw (tv.Bounds);
			expected = @"
┌─┬─┐
│A│B│
├─┼─┤
│1│5│
";
			TestHelpers.AssertDriverContentsAre (expected, output);


			expectedColors = @"
00000
00000
00000
01000
";

			// now we only see 2 colors used (the selected cell color and Normal
			// rowHighlight should no longer be used because the delegate returned null
			// (now that the cell value is 5 - which does not match the conditional)
			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
				// 0
				tv.ColorScheme.Normal,
				// 1
				focused ? tv.ColorScheme.HotFocus : tv.ColorScheme.HotNormal });


			// Shutdown must be called to safely clean up Application if Init has been called
			Application.Shutdown ();
		}

		[Theory]
		[InlineData (false)]
		[InlineData (true)]
		public void TableView_ColorsTest_ColorGetter (bool focused)
		{
			var tv = SetUpMiniTable ();

			// width exactly matches the max col widths
			tv.Bounds = new Rect (0, 0, 5, 4);

			// Create a style for column B
			var bStyle = tv.Style.GetOrCreateColumnStyle (tv.Table.Columns ["B"]);

			// when B is 2 use the custom highlight colour
			var cellHighlight = new ColorScheme () {
				Normal = Attribute.Make (Color.BrightCyan, Color.DarkGray),
				HotNormal = Attribute.Make (Color.Green, Color.Blue),
				HotFocus = Attribute.Make (Color.BrightYellow, Color.White),
				Focus = Attribute.Make (Color.Cyan, Color.Magenta),
			};

			bStyle.ColorGetter = (a) => Convert.ToInt32(a.CellValue) == 2 ? cellHighlight : null;

			// private method for forcing the view to be focused/not focused
			var setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic);

			// when the view is/isn't focused 
			setFocusMethod.Invoke (tv, new object [] { focused, tv, true });

			tv.Redraw (tv.Bounds);

			string expected = @"
┌─┬─┐
│A│B│
├─┼─┤
│1│2│
";
			TestHelpers.AssertDriverContentsAre (expected, output);


			string expectedColors = @"
00000
00000
00000
01020
";
			
			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
				// 0
				tv.ColorScheme.Normal,				
				// 1
				focused ? tv.ColorScheme.HotFocus : tv.ColorScheme.HotNormal,
				// 2
				cellHighlight.Normal});


			// change the value in the table so that
			// it no longer matches the ColorGetter
			// delegate conditional ( which checks for
			// the value 2)
			tv.Table.Rows[0][1] = 5;

			tv.Redraw (tv.Bounds);
			expected = @"
┌─┬─┐
│A│B│
├─┼─┤
│1│5│
";
			TestHelpers.AssertDriverContentsAre (expected, output);


			expectedColors = @"
00000
00000
00000
01000
";

			// now we only see 2 colors used (the selected cell color and Normal
			// cellHighlight should no longer be used because the delegate returned null
			// (now that the cell value is 5 - which does not match the conditional)
			TestHelpers.AssertDriverColorsAre (expectedColors, new Attribute [] {
				// 0
				tv.ColorScheme.Normal,				
				// 1
				focused ? tv.ColorScheme.HotFocus : tv.ColorScheme.HotNormal });


			// Shutdown must be called to safely clean up Application if Init has been called
			Application.Shutdown ();
		}

		private TableView SetUpMiniTable ()
		{

			var tv = new TableView ();
			tv.Bounds = new Rect (0, 0, 10, 4);

			var dt = new DataTable ();
			var colA = dt.Columns.Add ("A");
			var colB = dt.Columns.Add ("B");
			dt.Rows.Add (1, 2);

			tv.Table = dt;
			tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1;
			tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1;
			tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1;
			tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1;

			GraphViewTests.InitFakeDriver ();
			tv.ColorScheme = Colors.Base;
			return tv;
		}

		[Fact]
		[AutoInitShutdown]
		public void ScrollDown_OneLineAtATime ()
		{
			var tableView = new TableView ();

			// Set big table
			tableView.Table = BuildTable (25, 50);

			// 1 header + 4 rows visible
			tableView.Bounds = new Rect (0, 0, 25, 5);
			tableView.Style.ShowHorizontalHeaderUnderline = false;
			tableView.Style.ShowHorizontalHeaderOverline = false;
			tableView.Style.AlwaysShowHeaders = true;

			// select last row
			tableView.SelectedRow = 3; // row is 0 indexed so this is the 4th visible row

			// Scroll down
			tableView.ProcessKey (new KeyEvent () { Key = Key.CursorDown });

			// Scrolled off the page by 1 row so it should only have moved down 1 line of RowOffset
			Assert.Equal(4,tableView.SelectedRow);
			Assert.Equal (1, tableView.RowOffset);
		}

		[Fact]
		public void ScrollRight_SmoothScrolling ()
		{
			GraphViewTests.InitFakeDriver ();

			var tableView = new TableView ();
			tableView.ColorScheme = Colors.TopLevel;

			// 3 columns are visibile
			tableView.Bounds = new Rect (0, 0, 7, 5);
			tableView.Style.ShowHorizontalHeaderUnderline = false;
			tableView.Style.ShowHorizontalHeaderOverline = false;
			tableView.Style.AlwaysShowHeaders = true;
			tableView.Style.SmoothHorizontalScrolling = true;

			var dt = new DataTable ();
			dt.Columns.Add ("A");
			dt.Columns.Add ("B");
			dt.Columns.Add ("C");
			dt.Columns.Add ("D");
			dt.Columns.Add ("E");
			dt.Columns.Add ("F");

			dt.Rows.Add (1, 2, 3, 4, 5, 6);

			tableView.Table = dt;

			// select last visible column
			tableView.SelectedColumn = 2; // column C

			tableView.Redraw (tableView.Bounds);

			string expected = 
				@"
│A│B│C│
│1│2│3│";

			TestHelpers.AssertDriverContentsAre (expected, output);


			// Scroll right
			tableView.ProcessKey (new KeyEvent () { Key = Key.CursorRight });


			tableView.Redraw (tableView.Bounds);

			// Note that with SmoothHorizontalScrolling only a single new column
			// is exposed when scrolling right.  This is not always the case though
			// sometimes if the leftmost column is long (i.e. A is a long column)
			// then when A is pushed off the screen multiple new columns could be exposed
			// (not just D but also E and F).  This is because TableView never shows
			// 'half cells' or scrolls by console unit (scrolling is done by table row/column increments).

			expected =
				@"
│B│C│D│
│2│3│4│";

			TestHelpers.AssertDriverContentsAre (expected, output);


			// Shutdown must be called to safely clean up Application if Init has been called
			Application.Shutdown ();
		}

		[Fact]
		public void ScrollRight_WithoutSmoothScrolling ()
		{
			GraphViewTests.InitFakeDriver ();

			var tableView = new TableView ();
			tableView.ColorScheme = Colors.TopLevel;

			// 3 columns are visibile
			tableView.Bounds = new Rect (0, 0, 7, 5);
			tableView.Style.ShowHorizontalHeaderUnderline = false;
			tableView.Style.ShowHorizontalHeaderOverline = false;
			tableView.Style.AlwaysShowHeaders = true;
			tableView.Style.SmoothHorizontalScrolling = false;

			var dt = new DataTable ();
			dt.Columns.Add ("A");
			dt.Columns.Add ("B");
			dt.Columns.Add ("C");
			dt.Columns.Add ("D");
			dt.Columns.Add ("E");
			dt.Columns.Add ("F");

			dt.Rows.Add (1, 2, 3, 4, 5, 6);

			tableView.Table = dt;

			// select last visible column
			tableView.SelectedColumn = 2; // column C

			tableView.Redraw (tableView.Bounds);

			string expected =
				@"
│A│B│C│
│1│2│3│";

			TestHelpers.AssertDriverContentsAre (expected, output);


			// Scroll right
			tableView.ProcessKey (new KeyEvent () { Key = Key.CursorRight });


			tableView.Redraw (tableView.Bounds);

			// notice that without smooth scrolling we just update the first column
			// rendered in the table to the newly exposed column (D).  This is fast
			// since we don't have to worry about repeatedly measuring the content
			// area as we scroll until the new column (D) is exposed.  But it makes
			// the view 'jump' to expose all new columns


			expected =
				@"
│D│E│F│
│4│5│6│";

			TestHelpers.AssertDriverContentsAre (expected, output);


			// Shutdown must be called to safely clean up Application if Init has been called
			Application.Shutdown ();
		}

		private TableView GetABCDEFTableView (out DataTable dt)
		{
			var tableView = new TableView ();
			tableView.ColorScheme = Colors.TopLevel;

			// 3 columns are visible
			tableView.Bounds = new Rect (0, 0, 7, 5);
			tableView.Style.ShowHorizontalHeaderUnderline = false;
			tableView.Style.ShowHorizontalHeaderOverline = false;
			tableView.Style.AlwaysShowHeaders = true;
			tableView.Style.SmoothHorizontalScrolling = false;

			dt = new DataTable ();
			dt.Columns.Add ("A");
			dt.Columns.Add ("B");
			dt.Columns.Add ("C");
			dt.Columns.Add ("D");
			dt.Columns.Add ("E");
			dt.Columns.Add ("F");


			dt.Rows.Add (1, 2, 3, 4, 5, 6);
			tableView.Table = dt;

			return tableView;
		}

		[Fact, AutoInitShutdown]
		public void TestColumnStyle_VisibleFalse_IsNotRendered()
		{
			var tableView = GetABCDEFTableView (out DataTable dt);

			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;

			tableView.Redraw (tableView.Bounds);

			string expected =
				@"
│A│C│D│
│1│3│4│";

			TestHelpers.AssertDriverContentsAre (expected, output);
		}

		[Fact, AutoInitShutdown]
		public void TestColumnStyle_FirstColumnVisibleFalse_IsNotRendered ()
		{
			var tableView = GetABCDEFTableView (out DataTable dt);

			tableView.Style.ShowHorizontalScrollIndicators = true;
			tableView.Style.ShowHorizontalHeaderUnderline = true;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;

			tableView.Redraw (tableView.Bounds);

			string expected =
				@"
│B│C│D│
├─┼─┼─►
│2│3│4│";

			TestHelpers.AssertDriverContentsAre (expected, output);
		}


		[Fact, AutoInitShutdown]
		public void TestColumnStyle_AllColumnsVisibleFalse_BehavesAsTableNull ()
		{
			var tableView = GetABCDEFTableView (out DataTable dt);

			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["C"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;


			// expect nothing to be rendered when all columns are invisible
			string expected =
				@"
";

			tableView.Redraw (tableView.Bounds);
			TestHelpers.AssertDriverContentsAre (expected, output);


			// expect behavior to match when Table is null
			tableView.Table = null;

			tableView.Redraw (tableView.Bounds);
			TestHelpers.AssertDriverContentsAre (expected, output);
		}

		[Fact, AutoInitShutdown]
		public void TestColumnStyle_RemainingColumnsInvisible_NoScrollIndicator ()
		{
			var tableView = GetABCDEFTableView (out DataTable dt);

			tableView.Style.ShowHorizontalScrollIndicators = true;
			tableView.Style.ShowHorizontalHeaderUnderline = true;

			tableView.Redraw (tableView.Bounds);

			// normally we should have scroll indicators because DEF are of screen
			string expected =
				@"
│A│B│C│
├─┼─┼─►
│1│2│3│";

			TestHelpers.AssertDriverContentsAre (expected, output);

			// but if DEF are invisible we shouldn't be showing the indicator
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;

			expected =
			       @"
│A│B│C│
├─┼─┼─┤
│1│2│3│";
			tableView.Redraw (tableView.Bounds);
			TestHelpers.AssertDriverContentsAre (expected, output);
		}

		[Fact, AutoInitShutdown]
		public void TestColumnStyle_PreceedingColumnsInvisible_NoScrollIndicator ()
		{
			var tableView = GetABCDEFTableView (out DataTable dt);

			tableView.Style.ShowHorizontalScrollIndicators = true;
			tableView.Style.ShowHorizontalHeaderUnderline = true;

			tableView.ColumnOffset = 1;
			tableView.Redraw (tableView.Bounds);

			// normally we should have scroll indicators because A,E and F are of screen
			string expected =
				@"
│B│C│D│
◄─┼─┼─►
│2│3│4│";

			TestHelpers.AssertDriverContentsAre (expected, output);

			// but if E and F are invisible so we shouldn't show right
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;

			expected =
			       @"
│B│C│D│
◄─┼─┼─┤
│2│3│4│";
			tableView.Redraw (tableView.Bounds);
			TestHelpers.AssertDriverContentsAre (expected, output);

			// now also A is invisible so we cannot scroll in either direction
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;

			expected =
			       @"
│B│C│D│
├─┼─┼─┤
│2│3│4│";
			tableView.Redraw (tableView.Bounds);
			TestHelpers.AssertDriverContentsAre (expected, output);
		}
		[Fact, AutoInitShutdown]
		public void TestColumnStyle_VisibleFalse_CursorStepsOverInvisibleColumns ()
		{
			var tableView = GetABCDEFTableView (out var dt);
			
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;
			tableView.SelectedColumn = 0;

			tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight });

			// Expect the cursor navigation to skip over the invisible column(s)
			Assert.Equal(2,tableView.SelectedColumn);

			tableView.ProcessKey (new KeyEvent { Key = Key.CursorLeft });

			// Expect the cursor navigation backwards to skip over invisible column too
			Assert.Equal (0, tableView.SelectedColumn);
		}

		[InlineData(true)]
		[InlineData (false)]
		[Theory, AutoInitShutdown]
		public void TestColumnStyle_FirstColumnVisibleFalse_CursorStaysAt1(bool useHome)
		{
			var tableView = GetABCDEFTableView (out var dt);

			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["A"]).Visible = false;
			tableView.SelectedColumn = 0;

			Assert.Equal (0, tableView.SelectedColumn);

			// column 0 is invisible so this method should move to 1
			tableView.EnsureValidSelection();
			Assert.Equal (1, tableView.SelectedColumn);

			tableView.ProcessKey (new KeyEvent 
			{
				Key = useHome ? Key.Home : Key.CursorLeft 
			});

			// Expect the cursor to stay at 1
			Assert.Equal (1, tableView.SelectedColumn);
		}


		[InlineData(true)]
		[InlineData (false)]
		[Theory, AutoInitShutdown]
		public void TestMoveStartEnd_WithFullRowSelect(bool withFullRowSelect)
		{
			var tableView = GetTwoRowSixColumnTable ();
			tableView.FullRowSelect = withFullRowSelect;

			tableView.SelectedRow = 1;
			tableView.SelectedColumn = 1;

			tableView.ProcessKey (new KeyEvent 
			{
				Key = Key.Home  | Key.CtrlMask
			});

			if(withFullRowSelect)
			{
				// Should not be any horizontal movement when
				// using navigate to Start/End and FullRowSelect
				Assert.Equal (1, tableView.SelectedColumn);
				Assert.Equal (0, tableView.SelectedRow);
			}
			else
			{
				Assert.Equal (0, tableView.SelectedColumn);
				Assert.Equal (0, tableView.SelectedRow);
			}

			tableView.ProcessKey (new KeyEvent 
			{
				Key = Key.End  | Key.CtrlMask
			});

			if(withFullRowSelect)
			{
				Assert.Equal (1, tableView.SelectedColumn);
				Assert.Equal (1, tableView.SelectedRow);
			}
			else
			{
				Assert.Equal (5, tableView.SelectedColumn);
				Assert.Equal (1, tableView.SelectedRow);
			}

		}

		[InlineData (true)]
		[InlineData (false)]
		[Theory, AutoInitShutdown]
		public void TestColumnStyle_LastColumnVisibleFalse_CursorStaysAt2 (bool useEnd)
		{
			var tableView = GetABCDEFTableView (out var dt);
						
			// select D 
			tableView.SelectedColumn = 3;
			Assert.Equal (3, tableView.SelectedColumn);

			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["E"]).Visible = false;
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["F"]).Visible = false;

			// column D is invisible so this method should move to 2 (C)
			tableView.EnsureValidSelection ();
			Assert.Equal (2, tableView.SelectedColumn);

			tableView.ProcessKey (new KeyEvent {
				Key = useEnd ? Key.End : Key.CursorRight
			});

			// Expect the cursor to stay at 2
			Assert.Equal (2, tableView.SelectedColumn);
		}

		[Fact, AutoInitShutdown]
		public void TestColumnStyle_VisibleFalse_MultiSelected ()
		{
			var tableView = GetABCDEFTableView (out var dt);

			// user has rectangular selection 
			tableView.MultiSelectedRegions.Push (
				new TableView.TableSelection(
					new Point(0,0),
					new Rect(0, 0, 3, 1))
				);

			Assert.Equal (3, tableView.GetAllSelectedCells ().Count());
			Assert.True (tableView.IsSelected (0, 0));
			Assert.True (tableView.IsSelected (1, 0));
			Assert.True (tableView.IsSelected (2, 0));
			Assert.False (tableView.IsSelected (3, 0));

			// if middle column is invisible
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;

			// it should not be included in the selection
			Assert.Equal (2, tableView.GetAllSelectedCells ().Count ());
			Assert.True (tableView.IsSelected (0, 0));
			Assert.False (tableView.IsSelected (1, 0));
			Assert.True (tableView.IsSelected (2, 0));
			Assert.False (tableView.IsSelected (3, 0));

			Assert.DoesNotContain(new Point(1,0),tableView.GetAllSelectedCells ());
		}

		[Fact, AutoInitShutdown]
		public void TestColumnStyle_VisibleFalse_MultiSelectingStepsOverInvisibleColumns ()
		{
			var tableView = GetABCDEFTableView (out var dt);

			// if middle column is invisible
			tableView.Style.GetOrCreateColumnStyle (dt.Columns ["B"]).Visible = false;

			tableView.ProcessKey (new KeyEvent { Key = Key.CursorRight | Key.ShiftMask });

			// Selection should extend from A to C but skip B
			Assert.Equal (2, tableView.GetAllSelectedCells ().Count ());
			Assert.True (tableView.IsSelected (0, 0));
			Assert.False (tableView.IsSelected (1, 0));
			Assert.True (tableView.IsSelected (2, 0));
			Assert.False (tableView.IsSelected (3, 0));

			Assert.DoesNotContain (new Point (1, 0), tableView.GetAllSelectedCells ());
		}
		
		[Theory, AutoInitShutdown]
		[InlineData(new object[] { true,true })]
		[InlineData (new object[] { false,true })]
		[InlineData (new object [] { true, false})]
		[InlineData (new object [] { false, false})]
		public void TestColumnStyle_VisibleFalse_DoesNotEffect_EnsureSelectedCellIsVisible (bool smooth, bool invisibleCol)
		{
			var tableView = GetABCDEFTableView (out var dt);
			tableView.Style.SmoothHorizontalScrolling = smooth;
			
			if(invisibleCol) {
				tableView.Style.GetOrCreateColumnStyle (dt.Columns ["D"]).Visible = false;
			}

			// New TableView should have first cell selected 
			Assert.Equal (0,tableView.SelectedColumn);
			// With no scrolling
			Assert.Equal (0, tableView.ColumnOffset);

			// A,B and C are visible on screen at the moment so these should have no effect
			tableView.SelectedColumn = 1;
			tableView.EnsureSelectedCellIsVisible ();
			Assert.Equal (0, tableView.ColumnOffset);

			tableView.SelectedColumn = 2;
			tableView.EnsureSelectedCellIsVisible ();
			Assert.Equal (0, tableView.ColumnOffset);

			// Selecting D should move the visible table area to fit D onto the screen
			tableView.SelectedColumn = 3;
			tableView.EnsureSelectedCellIsVisible ();
			Assert.Equal (smooth ? 1 : 3, tableView.ColumnOffset);
		}
		[Fact]
		public void LongColumnTest ()
		{
			GraphViewTests.InitFakeDriver ();

			var tableView = new TableView ();
			tableView.ColorScheme = Colors.TopLevel;

			// 25 characters can be printed into table
			tableView.Bounds = new Rect (0, 0, 25, 5);
			tableView.Style.ShowHorizontalHeaderUnderline = true;
			tableView.Style.ShowHorizontalHeaderOverline = false;
			tableView.Style.AlwaysShowHeaders = true;
			tableView.Style.SmoothHorizontalScrolling = true;

			var dt = new DataTable ();
			dt.Columns.Add ("A");
			dt.Columns.Add ("B");
			dt.Columns.Add ("Very Long Column");

			dt.Rows.Add (1, 2, new string('a',500));
			dt.Rows.Add (1, 2, "aaa");

			tableView.Table = dt;

			tableView.Redraw (tableView.Bounds);

			// default behaviour of TableView is not to render
			// columns unless there is sufficient space
			string expected = 
				@"
│A│B                    │
├─┼─────────────────────►
│1│2                    │
│1│2                    │
";

			TestHelpers.AssertDriverContentsAre (expected, output);

			// get a style for the long column
			var style = tableView.Style.GetOrCreateColumnStyle(dt.Columns[2]);
			
			// one way the API user can fix this for long columns
			// is to specify a max width for the column
			style.MaxWidth = 10;

			tableView.Redraw (tableView.Bounds);
			expected = 
				@"
│A│B│Very Long          │
├─┼─┼───────────────────┤
│1│2│aaaaaaaaaa         │
│1│2│aaa                │
";
			TestHelpers.AssertDriverContentsAre (expected, output);

			// revert the style change
			style.MaxWidth = TableView.DefaultMaxCellWidth;

			// another way API user can fix problem is to implement
			// RepresentationGetter and apply max length there

			style.RepresentationGetter = (s)=>{
				return s.ToString().Length < 15 ? s.ToString() : s.ToString().Substring(0,13)+"...";
			};

			tableView.Redraw (tableView.Bounds);
			expected = 
				@"
│A│B│Very Long Column   │
├─┼─┼───────────────────┤
│1│2│aaaaaaaaaaaaa...   │
│1│2│aaa                │
";
			TestHelpers.AssertDriverContentsAre (expected, output);

			// revert style change
			style.RepresentationGetter = null;

			// Both of the above methods rely on having a fixed
			// size limit for the column.  These are awkward if a
			// table is resizeable e.g. Dim.Fill().  Ideally we want
			// to render in any space available and truncate the content
			// of the column dynamically so it fills the free space at
			// the end of the table.

			// We can now specify that the column can be any length
			// (Up to MaxWidth) but the renderer can accept using
			// less space down to this limit
			style.MinAcceptableWidth = 5;

			tableView.Redraw (tableView.Bounds);
			expected = 
				@"
│A│B│Very Long Column   │
├─┼─┼───────────────────┤
│1│2│aaaaaaaaaaaaaaaaaaa│
│1│2│aaa                │
";
			TestHelpers.AssertDriverContentsAre (expected, output);

			// Now test making the width too small for the MinAcceptableWidth
			// the Column won't fit so should not be rendered
			Application.Shutdown ();
			GraphViewTests.InitFakeDriver ();

			tableView.Bounds = new Rect(0,0,9,5);
			tableView.Redraw (tableView.Bounds);
			expected =
@"
│A│B    │
├─┼─────►
│1│2    │
│1│2    │

";
			TestHelpers.AssertDriverContentsAre (expected, output);

			// setting width to 10 leaves just enough space for the column to
			// meet MinAcceptableWidth of 5.  Column width includes terminator line
			// symbol (e.g. ┤ or │)
			tableView.Bounds = new Rect (0, 0, 10, 5);
			tableView.Redraw (tableView.Bounds);
			expected =
@"
│A│B│Very│
├─┼─┼────┤
│1│2│aaaa│
│1│2│aaa │
";
			TestHelpers.AssertDriverContentsAre (expected, output);

			Application.Shutdown ();
		}


		[Fact]
		public void ScrollIndicators ()
		{
			GraphViewTests.InitFakeDriver ();

			var tableView = new TableView ();
			tableView.ColorScheme = Colors.TopLevel;

			// 3 columns are visibile
			tableView.Bounds = new Rect (0, 0, 7, 5);
			tableView.Style.ShowHorizontalHeaderUnderline = true;
			tableView.Style.ShowHorizontalHeaderOverline = false;
			tableView.Style.AlwaysShowHeaders = true;
			tableView.Style.SmoothHorizontalScrolling = true;

			var dt = new DataTable ();
			dt.Columns.Add ("A");
			dt.Columns.Add ("B");
			dt.Columns.Add ("C");
			dt.Columns.Add ("D");
			dt.Columns.Add ("E");
			dt.Columns.Add ("F");

			dt.Rows.Add (1, 2, 3, 4, 5, 6);

			tableView.Table = dt;

			// select last visible column
			tableView.SelectedColumn = 2; // column C

			tableView.Redraw (tableView.Bounds);

			// user can only scroll right so sees right indicator
			// Because first column in table is A
			string expected = 
				@"
│A│B│C│
├─┼─┼─►
│1│2│3│";

			TestHelpers.AssertDriverContentsAre (expected, output);


			// Scroll right
			tableView.ProcessKey (new KeyEvent () { Key = Key.CursorRight });


			// since A is now pushed off screen we get indicator showing
			// that user can scroll left to see first column
			tableView.Redraw (tableView.Bounds);

			expected =
				@"
│B│C│D│
◄─┼─┼─►
│2│3│4│";

			TestHelpers.AssertDriverContentsAre (expected, output);


			// Scroll right twice more (to end of columns)
			tableView.ProcessKey (new KeyEvent () { Key = Key.CursorRight });
			tableView.ProcessKey (new KeyEvent () { Key = Key.CursorRight });

			tableView.Redraw (tableView.Bounds);

			expected =
				@"
│D│E│F│
◄─┼─┼─┤
│4│5│6│";

			TestHelpers.AssertDriverContentsAre (expected, output);

			// Shutdown must be called to safely clean up Application if Init has been called
			Application.Shutdown ();
		}

		/// <summary>
		/// Builds a simple table of string columns with the requested number of columns and rows
		/// </summary>
		/// <param name="cols"></param>
		/// <param name="rows"></param>
		/// <returns></returns>
		public static DataTable BuildTable (int cols, int rows)
		{
			var dt = new DataTable ();

			for (int c = 0; c < cols; c++) {
				dt.Columns.Add ("Col" + c);
			}

			for (int r = 0; r < rows; r++) {
				var newRow = dt.NewRow ();

				for (int c = 0; c < cols; c++) {
					newRow [c] = $"R{r}C{c}";
				}

				dt.Rows.Add (newRow);
			}

			return dt;
		}

		[Fact, AutoInitShutdown]
		public void Test_ScreenToCell ()
		{
			var tableView = GetTwoRowSixColumnTable ();

			tableView.Redraw (tableView.Bounds);

			// user can only scroll right so sees right indicator
			// Because first column in table is A
			string expected =
				@"
│A│B│C│
├─┼─┼─►
│1│2│3│
│1│2│3│";

			TestHelpers.AssertDriverContentsAre (expected, output);

			// ---------------- X=0 -----------------------
			// click is before first cell
			Assert.Null (tableView.ScreenToCell (0, 0));
			Assert.Null (tableView.ScreenToCell (0, 1));
			Assert.Null (tableView.ScreenToCell (0, 2));
			Assert.Null (tableView.ScreenToCell (0, 3));
			Assert.Null (tableView.ScreenToCell (0, 4));

			// ---------------- X=1 -----------------------
			// click in header
			Assert.Null (tableView.ScreenToCell (1, 0));
			// click in header row line
			Assert.Null (tableView.ScreenToCell (1, 1));
			// click in cell 0,0
			Assert.Equal (new Point(0,0),tableView.ScreenToCell (1, 2));
			// click in cell 0,1
			Assert.Equal (new Point (0, 1), tableView.ScreenToCell (1, 3));
			// after last row
			Assert.Null (tableView.ScreenToCell (1, 4));


			// ---------------- X=2 -----------------------
			// ( even though there is a horizontal dividing line here we treat it as a hit on the cell before)
			// click in header
			Assert.Null (tableView.ScreenToCell (2, 0));
			// click in header row line
			Assert.Null (tableView.ScreenToCell (2, 1));
			// click in cell 0,0
			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (2, 2));
			// click in cell 0,1
			Assert.Equal (new Point (0, 1), tableView.ScreenToCell (2, 3));
			// after last row
			Assert.Null (tableView.ScreenToCell (2, 4));


			// ---------------- X=3 -----------------------
			// click in header
			Assert.Null (tableView.ScreenToCell (3, 0));
			// click in header row line
			Assert.Null (tableView.ScreenToCell (3, 1));
			// click in cell 1,0
			Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2));
			// click in cell 1,1
			Assert.Equal (new Point (1, 1), tableView.ScreenToCell (3, 3));
			// after last row
			Assert.Null (tableView.ScreenToCell (3, 4));
		}

		[Fact, AutoInitShutdown]
		public void Test_ScreenToCell_DataColumnOverload ()
		{
			var tableView = GetTwoRowSixColumnTable ();

			tableView.Redraw (tableView.Bounds);

			// user can only scroll right so sees right indicator
			// Because first column in table is A
			string expected =
				@"
│A│B│C│
├─┼─┼─►
│1│2│3│
│1│2│3│";

			TestHelpers.AssertDriverContentsAre (expected, output);
			DataColumn col;

			// ---------------- X=0 -----------------------
			// click is before first cell
			Assert.Null (tableView.ScreenToCell (0, 0,out col));
			Assert.Null (col);
			Assert.Null (tableView.ScreenToCell (0, 1,out col));
			Assert.Null (col);
			Assert.Null (tableView.ScreenToCell (0, 2,out col));
			Assert.Null (col);
			Assert.Null (tableView.ScreenToCell (0, 3,out col));
			Assert.Null (col);
			Assert.Null (tableView.ScreenToCell (0, 4,out col));
			Assert.Null (col);

			// ---------------- X=1 -----------------------
			// click in header
			Assert.Null (tableView.ScreenToCell (1, 0, out col));
			Assert.Equal ("A", col.ColumnName);
			// click in header row line  (click in the horizontal line below header counts as click in header above - consistent with the column hit box)
			Assert.Null (tableView.ScreenToCell (1, 1, out col));
			Assert.Equal ("A", col.ColumnName);
			// click in cell 0,0
			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (1, 2, out col));
			Assert.Null (col);
			// click in cell 0,1
			Assert.Equal (new Point (0, 1), tableView.ScreenToCell (1, 3, out col));
			Assert.Null (col);
			// after last row
			Assert.Null (tableView.ScreenToCell (1, 4, out col));
			Assert.Null (col);


			// ---------------- X=2 -----------------------
			// click in header
			Assert.Null (tableView.ScreenToCell (2, 0, out col));
			Assert.Equal ("A", col.ColumnName);
			// click in header row line
			Assert.Null (tableView.ScreenToCell (2, 1, out col));
			Assert.Equal ("A", col.ColumnName);
			// click in cell 0,0
			Assert.Equal (new Point (0, 0), tableView.ScreenToCell (2, 2, out col));
			Assert.Null (col);
			// click in cell 0,1
			Assert.Equal (new Point (0, 1), tableView.ScreenToCell (2, 3, out col));
			Assert.Null (col);
			// after last row
			Assert.Null (tableView.ScreenToCell (2, 4, out col));
			Assert.Null (col);


			// ---------------- X=3 -----------------------
			// click in header
			Assert.Null (tableView.ScreenToCell (3, 0, out col));
			Assert.Equal ("B", col.ColumnName);
			// click in header row line
			Assert.Null (tableView.ScreenToCell (3, 1, out col));
			Assert.Equal ("B", col.ColumnName);
			// click in cell 1,0
			Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2, out col));
			Assert.Null (col);
			// click in cell 1,1
			Assert.Equal (new Point (1, 1), tableView.ScreenToCell (3, 3, out col));
			Assert.Null (col);
			// after last row
			Assert.Null (tableView.ScreenToCell (3, 4, out col));
			Assert.Null (col);
		}
		private TableView GetTwoRowSixColumnTable ()
		{
			var tableView = new TableView ();
			tableView.ColorScheme = Colors.TopLevel;

			// 3 columns are visible
			tableView.Bounds = new Rect (0, 0, 7, 5);
			tableView.Style.ShowHorizontalHeaderUnderline = true;
			tableView.Style.ShowHorizontalHeaderOverline = false;
			tableView.Style.AlwaysShowHeaders = true;
			tableView.Style.SmoothHorizontalScrolling = true;

			var dt = new DataTable ();
			dt.Columns.Add ("A");
			dt.Columns.Add ("B");
			dt.Columns.Add ("C");
			dt.Columns.Add ("D");
			dt.Columns.Add ("E");
			dt.Columns.Add ("F");

			dt.Rows.Add (1, 2, 3, 4, 5, 6);
			dt.Rows.Add (1, 2, 3, 4, 5, 6);

			tableView.Table = dt;
			return tableView;
		}
	}
}